Fastapi refactor update
This commit is contained in:
@@ -1,20 +1,11 @@
|
||||
from .level import ILevelService
|
||||
from .listening import IListeningService
|
||||
from .writing import IWritingService
|
||||
from .speaking import ISpeakingService
|
||||
from .reading import IReadingService
|
||||
from .grade import IGradeService
|
||||
from .training import ITrainingService
|
||||
from .kb import IKnowledgeBase
|
||||
from .third_parties import *
|
||||
|
||||
__all__ = [
|
||||
"ILevelService",
|
||||
"IListeningService",
|
||||
"IWritingService",
|
||||
"ISpeakingService",
|
||||
"IReadingService",
|
||||
"IGradeService",
|
||||
"ITrainingService"
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
from .third_parties import *
|
||||
from .exam import *
|
||||
from .training import *
|
||||
from .user import IUserService
|
||||
|
||||
__all__ = [
|
||||
"IUserService"
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
__all__.extend(exam.__all__)
|
||||
__all__.extend(training.__all__)
|
||||
|
||||
15
app/services/abc/exam/__init__.py
Normal file
15
app/services/abc/exam/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from .level import ILevelService
|
||||
from .listening import IListeningService
|
||||
from .writing import IWritingService
|
||||
from .speaking import ISpeakingService
|
||||
from .reading import IReadingService
|
||||
from .grade import IGradeService
|
||||
|
||||
__all__ = [
|
||||
"ILevelService",
|
||||
"IListeningService",
|
||||
"IWritingService",
|
||||
"ISpeakingService",
|
||||
"IReadingService",
|
||||
"IGradeService",
|
||||
]
|
||||
@@ -1,13 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IGradeService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class IGradeService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
pass
|
||||
@@ -1,47 +1,47 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.configs.constants import EducationalContent
|
||||
|
||||
|
||||
class ILevelService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_exam(
|
||||
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
|
||||
) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_utas(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_custom_level(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_level(self, upload: UploadFile) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_multiple_choice(
|
||||
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)
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_reading_passage_utas(
|
||||
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.configs.constants import EducationalContent
|
||||
|
||||
|
||||
class ILevelService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_exam(
|
||||
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
|
||||
) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_level_utas(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_custom_level(self, data: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_level(self, upload: UploadFile) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_multiple_choice(
|
||||
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)
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def gen_reading_passage_utas(
|
||||
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
pass
|
||||
@@ -1,18 +1,18 @@
|
||||
import queue
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_listening(self, parts: list[dict], min_timer: int, difficulty: str, listening_id: str) -> Dict:
|
||||
pass
|
||||
import queue
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_listening(self, parts: list[dict], min_timer: int, difficulty: str, listening_id: str) -> Dict:
|
||||
pass
|
||||
@@ -1,22 +1,22 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import List
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_reading_passage(self, part: int, topic: str, word_count: int = 800):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from queue import Queue
|
||||
from typing import List
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_reading_passage(self, part: int, topic: str, word_count: int = 800):
|
||||
pass
|
||||
@@ -1,29 +1,29 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class ISpeakingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_speaking_part(
|
||||
self, part: int, topic: str, difficulty: str, second_topic: Optional[str] = None
|
||||
) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_videos_and_save_to_db(self, exercises: List[Dict], template: Dict, req_id: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_video(
|
||||
self, part: int, avatar: str, topic: str, questions: list[str],
|
||||
*,
|
||||
second_topic: Optional[str] = None,
|
||||
prompts: Optional[list[str]] = None,
|
||||
suffix: Optional[str] = None,
|
||||
):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class ISpeakingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_speaking_part(
|
||||
self, part: int, topic: str, difficulty: str, second_topic: Optional[str] = None
|
||||
) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_speaking_task(self, task: int, answers: List[Dict]) -> Dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_videos_and_save_to_db(self, exercises: List[Dict], template: Dict, req_id: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def generate_video(
|
||||
self, part: int, avatar: str, topic: str, questions: list[str],
|
||||
*,
|
||||
second_topic: Optional[str] = None,
|
||||
prompts: Optional[list[str]] = None,
|
||||
suffix: Optional[str] = None,
|
||||
):
|
||||
pass
|
||||
@@ -1,11 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class IWritingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_writing_task(self, task: int, question: str, answer: str):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class IWritingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def grade_writing_task(self, task: int, question: str, answer: str):
|
||||
pass
|
||||
@@ -1,13 +1,13 @@
|
||||
from .stt import ISpeechToTextService
|
||||
from .tts import ITextToSpeechService
|
||||
from .llm import ILLMService
|
||||
from .vid_gen import IVideoGeneratorService
|
||||
from .ai_detector import IAIDetectorService
|
||||
|
||||
__all__ = [
|
||||
"ISpeechToTextService",
|
||||
"ITextToSpeechService",
|
||||
"ILLMService",
|
||||
"IVideoGeneratorService",
|
||||
"IAIDetectorService"
|
||||
]
|
||||
from .stt import ISpeechToTextService
|
||||
from .tts import ITextToSpeechService
|
||||
from .llm import ILLMService
|
||||
from .vid_gen import IVideoGeneratorService
|
||||
from .ai_detector import IAIDetectorService
|
||||
|
||||
__all__ = [
|
||||
"ISpeechToTextService",
|
||||
"ITextToSpeechService",
|
||||
"ILLMService",
|
||||
"IVideoGeneratorService",
|
||||
"IAIDetectorService"
|
||||
]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class IAIDetectorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def run_detection(self, text: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _parse_detection(self, response: Dict) -> Optional[Dict]:
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class IAIDetectorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def run_detection(self, text: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _parse_detection(self, response: Dict) -> Optional[Dict]:
|
||||
pass
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, TypeVar, Callable
|
||||
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
class ILLMService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List,
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
check_blacklisted: bool = True,
|
||||
token_count: int = -1
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def prediction_override(self, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def pydantic_prediction(
|
||||
self,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
map_to_model: Callable,
|
||||
json_scheme: str,
|
||||
*,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_retries: int = 3
|
||||
) -> List[T] | T | None:
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, TypeVar, Callable
|
||||
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
class ILLMService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List,
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
check_blacklisted: bool = True,
|
||||
token_count: int = -1
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def prediction_override(self, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def pydantic_prediction(
|
||||
self,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
map_to_model: Callable,
|
||||
json_scheme: str,
|
||||
*,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_retries: int = 3
|
||||
) -> List[T] | T | None:
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ISpeechToTextService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def speech_to_text(self, file_path):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ISpeechToTextService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def speech_to_text(self, file_path):
|
||||
pass
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Union
|
||||
|
||||
|
||||
class ITextToSpeechService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def synthesize_speech(self, text: str, voice: str, engine: str, output_format: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def text_to_speech(self, text: Union[list[str], str], file_name: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _conversation_to_speech(self, conversation: list):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _text_to_speech(self, text: str):
|
||||
pass
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Union
|
||||
|
||||
|
||||
class ITextToSpeechService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def synthesize_speech(self, text: str, voice: str, engine: str, output_format: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def text_to_speech(self, text: Union[list[str], str], file_name: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _conversation_to_speech(self, conversation: list):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _text_to_speech(self, text: str):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from app.configs.constants import AvatarEnum
|
||||
|
||||
|
||||
class IVideoGeneratorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def create_video(self, text: str, avatar: str):
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from app.configs.constants import AvatarEnum
|
||||
|
||||
|
||||
class IVideoGeneratorService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def create_video(self, text: str, avatar: str):
|
||||
pass
|
||||
|
||||
7
app/services/abc/training/__init__.py
Normal file
7
app/services/abc/training/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .training import ITrainingService
|
||||
from .kb import IKnowledgeBase
|
||||
|
||||
__all__ = [
|
||||
"ITrainingService",
|
||||
"IKnowledgeBase"
|
||||
]
|
||||
@@ -1,10 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
class IKnowledgeBase(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def query_knowledge_base(self, query: str, category: str, top_k: int = 5) -> List[Dict[str, str]]:
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
class IKnowledgeBase(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def query_knowledge_base(self, query: str, category: str, top_k: int = 5) -> List[Dict[str, str]]:
|
||||
pass
|
||||
@@ -1,14 +1,14 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class ITrainingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, context: str, question: str, answer: str, correct_answer: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_training_content(self, training_content: Dict) -> Dict:
|
||||
pass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class ITrainingService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, context: str, question: str, answer: str, correct_answer: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_training_content(self, training_content: Dict) -> Dict:
|
||||
pass
|
||||
10
app/services/abc/user.py
Normal file
10
app/services/abc/user.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from app.dtos.user_batch import BatchUsersDTO
|
||||
|
||||
|
||||
class IUserService(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_tips(self, batch: BatchUsersDTO):
|
||||
pass
|
||||
@@ -1,19 +1,11 @@
|
||||
from .level import LevelService
|
||||
from .listening import ListeningService
|
||||
from .reading import ReadingService
|
||||
from .speaking import SpeakingService
|
||||
from .writing import WritingService
|
||||
from .grade import GradeService
|
||||
from .training import *
|
||||
from .third_parties import *
|
||||
|
||||
__all__ = [
|
||||
"LevelService",
|
||||
"ListeningService",
|
||||
"ReadingService",
|
||||
"SpeakingService",
|
||||
"WritingService",
|
||||
"GradeService",
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
__all__.extend(training.__all__)
|
||||
from .user import UserService
|
||||
from .training import *
|
||||
from .third_parties import *
|
||||
from .exam import *
|
||||
|
||||
__all__ = [
|
||||
"UserService"
|
||||
]
|
||||
__all__.extend(third_parties.__all__)
|
||||
__all__.extend(training.__all__)
|
||||
__all__.extend(exam.__all__)
|
||||
|
||||
16
app/services/impl/exam/__init__.py
Normal file
16
app/services/impl/exam/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from .level import LevelService
|
||||
from .listening import ListeningService
|
||||
from .reading import ReadingService
|
||||
from .speaking import SpeakingService
|
||||
from .writing import WritingService
|
||||
from .grade import GradeService
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LevelService",
|
||||
"ListeningService",
|
||||
"ReadingService",
|
||||
"SpeakingService",
|
||||
"WritingService",
|
||||
"GradeService",
|
||||
]
|
||||
@@ -1,200 +1,200 @@
|
||||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
from app.configs.constants import GPTModels, TemperatureSettings
|
||||
from app.services.abc import ILLMService, IGradeService
|
||||
|
||||
|
||||
class GradeService(IGradeService):
|
||||
|
||||
def __init__(self, llm: ILLMService):
|
||||
self._llm = llm
|
||||
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
json_format = {
|
||||
"exercises": [
|
||||
{
|
||||
"id": 1,
|
||||
"correct": True,
|
||||
"correct_answer": " correct answer if wrong"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_format}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Grade these answers according to the text content and write a correct answer if they are '
|
||||
f'wrong. Text, questions and answers:\n {data}'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["exercises"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
ret = []
|
||||
|
||||
for section in extracted_sections:
|
||||
openai_response_dict = await self._calculate_section_grade_summary(section)
|
||||
ret.append(
|
||||
{
|
||||
'code': section['code'],
|
||||
'name': section['name'],
|
||||
'grade': section['grade'],
|
||||
'evaluation': openai_response_dict['evaluation'],
|
||||
'suggestions': openai_response_dict['suggestions'],
|
||||
'bullet_points': self._parse_bullet_points(openai_response_dict['bullet_points'], section['grade'])
|
||||
}
|
||||
)
|
||||
|
||||
return {'sections': ret}
|
||||
|
||||
async def _calculate_section_grade_summary(self, section):
|
||||
section_name = section['name']
|
||||
section_grade = section['grade']
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'You are a IELTS test section grade evaluator. You will receive a IELTS test section name and the '
|
||||
'grade obtained in the section. You should offer a evaluation comment on this grade and separately '
|
||||
'suggestions on how to possibly get a better grade.'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Section: {str(section_name)} Grade: {str(section_grade)}',
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Speak in third person."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Don't offer suggestions in the evaluation comment. Only in the suggestions section."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Your evaluation comment on the grade should enunciate the grade, be insightful, be speculative, "
|
||||
"be one paragraph long."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Please save the evaluation comment and suggestions generated."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Offer bullet points to improve the english {str(section_name)} ability."
|
||||
},
|
||||
]
|
||||
|
||||
if section['code'] == "level":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is comprised of multiple choice questions that measure the user's overall english "
|
||||
"level. These multiple choice questions are about knowledge on vocabulary, syntax, grammar rules, "
|
||||
"and contextual usage. The grade obtained measures the ability in these areas and english language "
|
||||
"overall."
|
||||
)
|
||||
}]
|
||||
elif section['code'] == "speaking":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is s designed to assess the English language proficiency of individuals who want to "
|
||||
"study or work in English-speaking countries. The speaking section evaluates a candidate's ability "
|
||||
"to communicate effectively in spoken English."
|
||||
)
|
||||
}]
|
||||
|
||||
chat_config = {'max_tokens': 1000, 'temperature': 0.2}
|
||||
tools = self.get_tools()
|
||||
|
||||
res = await self._llm.prediction_override(
|
||||
model="gpt-3.5-turbo",
|
||||
max_tokens=chat_config['max_tokens'],
|
||||
temperature=chat_config['temperature'],
|
||||
tools=tools,
|
||||
messages=messages
|
||||
)
|
||||
|
||||
return self._parse_openai_response(res)
|
||||
|
||||
@staticmethod
|
||||
def _parse_openai_response(response):
|
||||
if 'choices' in response and len(response['choices']) > 0 and 'message' in response['choices'][
|
||||
0] and 'tool_calls' in response['choices'][0]['message'] and isinstance(
|
||||
response['choices'][0]['message']['tool_calls'], list) and len(
|
||||
response['choices'][0]['message']['tool_calls']) > 0 and \
|
||||
response['choices'][0]['message']['tool_calls'][0]['function']['arguments']:
|
||||
return json.loads(response['choices'][0]['message']['tool_calls'][0]['function']['arguments'])
|
||||
else:
|
||||
return {'evaluation': "", 'suggestions': "", 'bullet_points': []}
|
||||
|
||||
@staticmethod
|
||||
def _parse_bullet_points(bullet_points_str, grade):
|
||||
max_grade_for_suggestions = 9
|
||||
if isinstance(bullet_points_str, str) and grade < max_grade_for_suggestions:
|
||||
# Split the string by '\n'
|
||||
lines = bullet_points_str.split('\n')
|
||||
|
||||
# Remove '-' and trim whitespace from each line
|
||||
cleaned_lines = [line.replace('-', '').strip() for line in lines]
|
||||
|
||||
# Add '.' to lines that don't end with it
|
||||
return [line + '.' if line and not line.endswith('.') else line for line in cleaned_lines]
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_tools():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "save_evaluation_and_suggestions",
|
||||
"description": "Saves the evaluation and suggestions requested by input.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"evaluation": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"A comment on the IELTS section grade obtained in the specific section and what "
|
||||
"it could mean without suggestions."
|
||||
),
|
||||
},
|
||||
"suggestions": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"A small paragraph text with suggestions on how to possibly get a better grade "
|
||||
"than the one obtained."
|
||||
),
|
||||
},
|
||||
"bullet_points": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Text with four bullet points to improve the english speaking ability. Only "
|
||||
"include text for the bullet points separated by a paragraph."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["evaluation", "suggestions"],
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
from app.configs.constants import GPTModels, TemperatureSettings
|
||||
from app.services.abc import ILLMService, IGradeService
|
||||
|
||||
|
||||
class GradeService(IGradeService):
|
||||
|
||||
def __init__(self, llm: ILLMService):
|
||||
self._llm = llm
|
||||
|
||||
async def grade_short_answers(self, data: Dict):
|
||||
json_format = {
|
||||
"exercises": [
|
||||
{
|
||||
"id": 1,
|
||||
"correct": True,
|
||||
"correct_answer": " correct answer if wrong"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_format}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Grade these answers according to the text content and write a correct answer if they are '
|
||||
f'wrong. Text, questions and answers:\n {data}'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["exercises"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
async def calculate_grading_summary(self, extracted_sections: List):
|
||||
ret = []
|
||||
|
||||
for section in extracted_sections:
|
||||
openai_response_dict = await self._calculate_section_grade_summary(section)
|
||||
ret.append(
|
||||
{
|
||||
'code': section['code'],
|
||||
'name': section['name'],
|
||||
'grade': section['grade'],
|
||||
'evaluation': openai_response_dict['evaluation'],
|
||||
'suggestions': openai_response_dict['suggestions'],
|
||||
'bullet_points': self._parse_bullet_points(openai_response_dict['bullet_points'], section['grade'])
|
||||
}
|
||||
)
|
||||
|
||||
return {'sections': ret}
|
||||
|
||||
async def _calculate_section_grade_summary(self, section):
|
||||
section_name = section['name']
|
||||
section_grade = section['grade']
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'You are a IELTS test section grade evaluator. You will receive a IELTS test section name and the '
|
||||
'grade obtained in the section. You should offer a evaluation comment on this grade and separately '
|
||||
'suggestions on how to possibly get a better grade.'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Section: {str(section_name)} Grade: {str(section_grade)}',
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Speak in third person."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Don't offer suggestions in the evaluation comment. Only in the suggestions section."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Your evaluation comment on the grade should enunciate the grade, be insightful, be speculative, "
|
||||
"be one paragraph long."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Please save the evaluation comment and suggestions generated."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Offer bullet points to improve the english {str(section_name)} ability."
|
||||
},
|
||||
]
|
||||
|
||||
if section['code'] == "level":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is comprised of multiple choice questions that measure the user's overall english "
|
||||
"level. These multiple choice questions are about knowledge on vocabulary, syntax, grammar rules, "
|
||||
"and contextual usage. The grade obtained measures the ability in these areas and english language "
|
||||
"overall."
|
||||
)
|
||||
}]
|
||||
elif section['code'] == "speaking":
|
||||
messages[2:2] = [{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"This section is s designed to assess the English language proficiency of individuals who want to "
|
||||
"study or work in English-speaking countries. The speaking section evaluates a candidate's ability "
|
||||
"to communicate effectively in spoken English."
|
||||
)
|
||||
}]
|
||||
|
||||
chat_config = {'max_tokens': 1000, 'temperature': 0.2}
|
||||
tools = self.get_tools()
|
||||
|
||||
res = await self._llm.prediction_override(
|
||||
model="gpt-3.5-turbo",
|
||||
max_tokens=chat_config['max_tokens'],
|
||||
temperature=chat_config['temperature'],
|
||||
tools=tools,
|
||||
messages=messages
|
||||
)
|
||||
|
||||
return self._parse_openai_response(res)
|
||||
|
||||
@staticmethod
|
||||
def _parse_openai_response(response):
|
||||
if 'choices' in response and len(response['choices']) > 0 and 'message' in response['choices'][
|
||||
0] and 'tool_calls' in response['choices'][0]['message'] and isinstance(
|
||||
response['choices'][0]['message']['tool_calls'], list) and len(
|
||||
response['choices'][0]['message']['tool_calls']) > 0 and \
|
||||
response['choices'][0]['message']['tool_calls'][0]['function']['arguments']:
|
||||
return json.loads(response['choices'][0]['message']['tool_calls'][0]['function']['arguments'])
|
||||
else:
|
||||
return {'evaluation': "", 'suggestions': "", 'bullet_points': []}
|
||||
|
||||
@staticmethod
|
||||
def _parse_bullet_points(bullet_points_str, grade):
|
||||
max_grade_for_suggestions = 9
|
||||
if isinstance(bullet_points_str, str) and grade < max_grade_for_suggestions:
|
||||
# Split the string by '\n'
|
||||
lines = bullet_points_str.split('\n')
|
||||
|
||||
# Remove '-' and trim whitespace from each line
|
||||
cleaned_lines = [line.replace('-', '').strip() for line in lines]
|
||||
|
||||
# Add '.' to lines that don't end with it
|
||||
return [line + '.' if line and not line.endswith('.') else line for line in cleaned_lines]
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_tools():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "save_evaluation_and_suggestions",
|
||||
"description": "Saves the evaluation and suggestions requested by input.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"evaluation": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"A comment on the IELTS section grade obtained in the specific section and what "
|
||||
"it could mean without suggestions."
|
||||
),
|
||||
},
|
||||
"suggestions": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"A small paragraph text with suggestions on how to possibly get a better grade "
|
||||
"than the one obtained."
|
||||
),
|
||||
},
|
||||
"bullet_points": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Text with four bullet points to improve the english speaking ability. Only "
|
||||
"include text for the bullet points separated by a paragraph."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["evaluation", "suggestions"],
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
from .level import LevelService
|
||||
|
||||
__all__ = [
|
||||
"LevelService"
|
||||
from .level import LevelService
|
||||
|
||||
__all__ = [
|
||||
"LevelService"
|
||||
]
|
||||
@@ -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
|
||||
@@ -1,417 +1,417 @@
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.configs.constants import GPTModels, TemperatureSettings, EducationalContent
|
||||
from app.helpers import ExercisesHelper
|
||||
from app.repositories.abc import IDocumentStore
|
||||
from app.services.abc import ILevelService, ILLMService, IReadingService, IWritingService, ISpeakingService, \
|
||||
IListeningService
|
||||
from .custom import CustomLevelModule
|
||||
from .upload import UploadLevelModule
|
||||
|
||||
|
||||
class LevelService(ILevelService):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: ILLMService,
|
||||
document_store: IDocumentStore,
|
||||
mc_variants: Dict,
|
||||
reading_service: IReadingService,
|
||||
writing_service: IWritingService,
|
||||
speaking_service: ISpeakingService,
|
||||
listening_service: IListeningService
|
||||
):
|
||||
self._llm = llm
|
||||
self._document_store = document_store
|
||||
self._reading_service = reading_service
|
||||
self._custom_module = CustomLevelModule(
|
||||
llm, self, reading_service, listening_service, writing_service, speaking_service
|
||||
)
|
||||
self._upload_module = UploadLevelModule(llm)
|
||||
|
||||
# TODO: normal and blank spaces only differ on "multiple choice blank space questions" in the prompt
|
||||
# mc_variants are stored in ./mc_variants.json
|
||||
self._mc_variants = mc_variants
|
||||
|
||||
async def upload_level(self, upload: UploadFile) -> Dict:
|
||||
return await self._upload_module.generate_level_from_file(upload)
|
||||
|
||||
async def get_custom_level(self, data: Dict):
|
||||
return await self._custom_module.get_custom_level(data)
|
||||
|
||||
async def get_level_exam(
|
||||
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
|
||||
) -> Dict:
|
||||
exercises = await self.gen_multiple_choice("normal", number_of_exercises, utas=False)
|
||||
return {
|
||||
"exercises": [exercises],
|
||||
"isDiagnostic": diagnostic,
|
||||
"minTimer": min_timer,
|
||||
"module": "level"
|
||||
}
|
||||
|
||||
async def get_level_utas(self, diagnostic: bool = False, min_timer: int = 25):
|
||||
# Formats
|
||||
mc = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Choose the correct word or group of words that completes the sentences.",
|
||||
"questions": None,
|
||||
"type": "multipleChoice",
|
||||
"part": 1
|
||||
}
|
||||
|
||||
umc = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Choose the underlined word or group of words that is not correct.",
|
||||
"questions": None,
|
||||
"type": "multipleChoice",
|
||||
"part": 2
|
||||
}
|
||||
|
||||
bs_1 = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and write the correct word for each space.",
|
||||
"questions": None,
|
||||
"type": "blankSpaceText",
|
||||
"part": 3
|
||||
}
|
||||
|
||||
bs_2 = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and write the correct word for each space.",
|
||||
"questions": None,
|
||||
"type": "blankSpaceText",
|
||||
"part": 4
|
||||
}
|
||||
|
||||
reading = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and answer the questions below.",
|
||||
"questions": None,
|
||||
"type": "readingExercises",
|
||||
"part": 5
|
||||
}
|
||||
|
||||
all_mc_questions = []
|
||||
|
||||
# PART 1
|
||||
# await self._gen_multiple_choice("normal", number_of_exercises, utas=False)
|
||||
mc_exercises1 = await self.gen_multiple_choice(
|
||||
"blank_space", 15, 1, utas=True, all_exams=all_mc_questions
|
||||
)
|
||||
print(json.dumps(mc_exercises1, indent=4))
|
||||
all_mc_questions.append(mc_exercises1)
|
||||
|
||||
# PART 2
|
||||
mc_exercises2 = await self.gen_multiple_choice(
|
||||
"blank_space", 15, 16, utas=True, all_exams=all_mc_questions
|
||||
)
|
||||
print(json.dumps(mc_exercises2, indent=4))
|
||||
all_mc_questions.append(mc_exercises2)
|
||||
|
||||
# PART 3
|
||||
mc_exercises3 = await self.gen_multiple_choice(
|
||||
"blank_space", 15, 31, utas=True, all_exams=all_mc_questions
|
||||
)
|
||||
print(json.dumps(mc_exercises3, indent=4))
|
||||
all_mc_questions.append(mc_exercises3)
|
||||
|
||||
mc_exercises = mc_exercises1['questions'] + mc_exercises2['questions'] + mc_exercises3['questions']
|
||||
print(json.dumps(mc_exercises, indent=4))
|
||||
mc["questions"] = mc_exercises
|
||||
|
||||
# Underlined mc
|
||||
underlined_mc = await self.gen_multiple_choice(
|
||||
"underline", 15, 46, utas=True, all_exams=all_mc_questions
|
||||
)
|
||||
print(json.dumps(underlined_mc, indent=4))
|
||||
umc["questions"] = underlined_mc
|
||||
|
||||
# Blank Space text 1
|
||||
blank_space_text_1 = await self.gen_blank_space_text_utas(12, 61, 250)
|
||||
print(json.dumps(blank_space_text_1, indent=4))
|
||||
bs_1["questions"] = blank_space_text_1
|
||||
|
||||
# Blank Space text 2
|
||||
blank_space_text_2 = await self.gen_blank_space_text_utas(14, 73, 350)
|
||||
print(json.dumps(blank_space_text_2, indent=4))
|
||||
bs_2["questions"] = blank_space_text_2
|
||||
|
||||
# Reading text
|
||||
reading_text = await self.gen_reading_passage_utas(87, 10, 4)
|
||||
print(json.dumps(reading_text, indent=4))
|
||||
reading["questions"] = reading_text
|
||||
|
||||
return {
|
||||
"exercises": {
|
||||
"blankSpaceMultipleChoice": mc,
|
||||
"underlinedMultipleChoice": umc,
|
||||
"blankSpaceText1": bs_1,
|
||||
"blankSpaceText2": bs_2,
|
||||
"readingExercises": reading,
|
||||
},
|
||||
"isDiagnostic": diagnostic,
|
||||
"minTimer": min_timer,
|
||||
"module": "level"
|
||||
}
|
||||
|
||||
async def gen_multiple_choice(
|
||||
self, mc_variant: str, quantity: int, start_id: int = 1, *, utas: bool = False, all_exams=None
|
||||
):
|
||||
mc_template = self._mc_variants[mc_variant]
|
||||
blank_mod = " blank space " if mc_variant == "blank_space" else " "
|
||||
|
||||
gen_multiple_choice_for_text: str = (
|
||||
'Generate {quantity} multiple choice{blank}questions of 4 options for an english level exam, some easy '
|
||||
'questions, some intermediate questions and some advanced questions. Ensure that the questions cover '
|
||||
'a range of topics such as verb tense, subject-verb agreement, pronoun usage, sentence structure, and '
|
||||
'punctuation. Make sure every question only has 1 correct answer.'
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f'You are a helpful assistant designed to output JSON on this format: {mc_template}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": gen_multiple_choice_for_text.format(quantity=str(quantity), blank=blank_mod)
|
||||
}
|
||||
]
|
||||
|
||||
if mc_variant == "underline":
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
'The type of multiple choice in the prompt has wrong words or group of words and the options '
|
||||
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
|
||||
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
|
||||
'the boss <u>is</u> nice."\n'
|
||||
'Options:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
|
||||
)
|
||||
})
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if len(question["questions"]) != quantity:
|
||||
return await self.gen_multiple_choice(mc_variant, quantity, start_id, utas=utas, all_exams=all_exams)
|
||||
else:
|
||||
if not utas:
|
||||
all_exams = await self._document_store.get_all("level")
|
||||
seen_keys = set()
|
||||
for i in range(len(question["questions"])):
|
||||
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
|
||||
all_exams, question["questions"][i], question, seen_keys, mc_variant, utas
|
||||
)
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Select the appropriate option.",
|
||||
"questions": ExercisesHelper.fix_exercise_ids(question, start_id)["questions"],
|
||||
"type": "multipleChoice",
|
||||
}
|
||||
else:
|
||||
if all_exams is not None:
|
||||
seen_keys = set()
|
||||
for i in range(len(question["questions"])):
|
||||
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
|
||||
all_exams, question["questions"][i], question, seen_keys, mc_variant, utas
|
||||
)
|
||||
response = ExercisesHelper.fix_exercise_ids(question, start_id)
|
||||
response["questions"] = ExercisesHelper.randomize_mc_options_order(response["questions"])
|
||||
return response
|
||||
|
||||
async def _generate_single_multiple_choice(self, mc_variant: str = "normal"):
|
||||
mc_template = self._mc_variants[mc_variant]["questions"][0]
|
||||
blank_mod = " blank space " if mc_variant == "blank_space" else " "
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f'You are a helpful assistant designed to output JSON on this format: {mc_template}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate 1 multiple choice {blank_mod} question of 4 options for an english level exam, '
|
||||
f'it can be easy, intermediate or advanced.'
|
||||
)
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
if mc_variant == "underline":
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
'The type of multiple choice in the prompt has wrong words or group of words and the options '
|
||||
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
|
||||
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
|
||||
'the boss <u>is</u> nice."\n'
|
||||
'Options:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
|
||||
)
|
||||
})
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["options"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return question
|
||||
|
||||
async def _replace_exercise_if_exists(
|
||||
self, all_exams, current_exercise, current_exam, seen_keys, mc_variant: str, utas: bool = False
|
||||
):
|
||||
# Extracting relevant fields for comparison
|
||||
key = (current_exercise['prompt'], tuple(sorted(option['text'] for option in current_exercise['options'])))
|
||||
# Check if the key is in the set
|
||||
if key in seen_keys:
|
||||
return await self._replace_exercise_if_exists(
|
||||
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam, seen_keys,
|
||||
mc_variant, utas
|
||||
)
|
||||
else:
|
||||
seen_keys.add(key)
|
||||
|
||||
if not utas:
|
||||
for exam in all_exams:
|
||||
exam_dict = exam.to_dict()
|
||||
if len(exam_dict.get("parts", [])) > 0:
|
||||
exercise_dict = exam_dict.get("parts", [])[0]
|
||||
if len(exercise_dict.get("exercises", [])) > 0:
|
||||
if any(
|
||||
exercise["prompt"] == current_exercise["prompt"] and
|
||||
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
|
||||
current_exercise["options"])
|
||||
for exercise in exercise_dict.get("exercises", [])[0]["questions"]
|
||||
):
|
||||
return await self._replace_exercise_if_exists(
|
||||
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam,
|
||||
seen_keys, mc_variant, utas
|
||||
)
|
||||
else:
|
||||
for exam in all_exams:
|
||||
if any(
|
||||
exercise["prompt"] == current_exercise["prompt"] and
|
||||
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
|
||||
current_exercise["options"])
|
||||
for exercise in exam.get("questions", [])
|
||||
):
|
||||
return await self._replace_exercise_if_exists(
|
||||
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam,
|
||||
seen_keys, mc_variant, utas
|
||||
)
|
||||
return current_exercise, seen_keys
|
||||
|
||||
async def gen_blank_space_text_utas(
|
||||
self, quantity: int, start_id: int, size: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
json_template = self._mc_variants["blank_space_text"]
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_template}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Generate a text of at least {size} words about the topic {topic}.'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'From the generated text choose {quantity} words (cannot be sequential words) to replace '
|
||||
'once with {{id}} where id starts on ' + str(start_id) + ' and is incremented for each word. '
|
||||
'The ids must be ordered throughout the text and the words must be replaced only once. '
|
||||
'Put the removed words and respective ids on the words array of the json in the correct order.'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["question"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return question["question"]
|
||||
|
||||
async def gen_reading_passage_utas(
|
||||
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
passage = await self._reading_service.generate_reading_passage(1, topic)
|
||||
short_answer = await self._gen_short_answer_utas(passage["text"], start_id, sa_quantity)
|
||||
mc_exercises = await self._gen_text_multiple_choice_utas(passage["text"], start_id + sa_quantity, mc_quantity)
|
||||
return {
|
||||
"exercises": {
|
||||
"shortAnswer": short_answer,
|
||||
"multipleChoice": mc_exercises,
|
||||
},
|
||||
"text": {
|
||||
"content": passage["text"],
|
||||
"title": passage["title"]
|
||||
}
|
||||
}
|
||||
|
||||
async def _gen_short_answer_utas(self, text: str, start_id: int, sa_quantity: int):
|
||||
json_format = {"questions": [{"id": 1, "question": "question", "possible_answers": ["answer_1", "answer_2"]}]}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_format}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate {sa_quantity} short answer questions, and the possible answers, must have '
|
||||
f'maximum 3 words per answer, about this text:\n"{text}"'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'The id starts at {start_id}.'
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return question["questions"]
|
||||
|
||||
async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int):
|
||||
json_template = self._mc_variants["text_mc_utas"]
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_template}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Generate {mc_quantity} multiple choice questions of 4 options for this text:\n{text}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": 'Make sure every question only has 1 correct answer.'
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if len(question["questions"]) != mc_quantity:
|
||||
return await self._gen_text_multiple_choice_utas(text, mc_quantity, start_id)
|
||||
else:
|
||||
response = ExercisesHelper.fix_exercise_ids(question, start_id)
|
||||
response["questions"] = ExercisesHelper.randomize_mc_options_order(response["questions"])
|
||||
return response
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.configs.constants import GPTModels, TemperatureSettings, EducationalContent
|
||||
from app.helpers import ExercisesHelper
|
||||
from app.repositories.abc import IDocumentStore
|
||||
from app.services.abc import ILevelService, ILLMService, IReadingService, IWritingService, ISpeakingService, \
|
||||
IListeningService
|
||||
from .custom import CustomLevelModule
|
||||
from .upload import UploadLevelModule
|
||||
|
||||
|
||||
class LevelService(ILevelService):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: ILLMService,
|
||||
document_store: IDocumentStore,
|
||||
mc_variants: Dict,
|
||||
reading_service: IReadingService,
|
||||
writing_service: IWritingService,
|
||||
speaking_service: ISpeakingService,
|
||||
listening_service: IListeningService
|
||||
):
|
||||
self._llm = llm
|
||||
self._document_store = document_store
|
||||
self._reading_service = reading_service
|
||||
self._custom_module = CustomLevelModule(
|
||||
llm, self, reading_service, listening_service, writing_service, speaking_service
|
||||
)
|
||||
self._upload_module = UploadLevelModule(llm)
|
||||
|
||||
# TODO: normal and blank spaces only differ on "multiple choice blank space questions" in the prompt
|
||||
# mc_variants are stored in ./mc_variants.json
|
||||
self._mc_variants = mc_variants
|
||||
|
||||
async def upload_level(self, upload: UploadFile) -> Dict:
|
||||
return await self._upload_module.generate_level_from_file(upload)
|
||||
|
||||
async def get_custom_level(self, data: Dict):
|
||||
return await self._custom_module.get_custom_level(data)
|
||||
|
||||
async def get_level_exam(
|
||||
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
|
||||
) -> Dict:
|
||||
exercises = await self.gen_multiple_choice("normal", number_of_exercises, utas=False)
|
||||
return {
|
||||
"exercises": [exercises],
|
||||
"isDiagnostic": diagnostic,
|
||||
"minTimer": min_timer,
|
||||
"module": "level"
|
||||
}
|
||||
|
||||
async def get_level_utas(self, diagnostic: bool = False, min_timer: int = 25):
|
||||
# Formats
|
||||
mc = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Choose the correct word or group of words that completes the sentences.",
|
||||
"questions": None,
|
||||
"type": "multipleChoice",
|
||||
"part": 1
|
||||
}
|
||||
|
||||
umc = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Choose the underlined word or group of words that is not correct.",
|
||||
"questions": None,
|
||||
"type": "multipleChoice",
|
||||
"part": 2
|
||||
}
|
||||
|
||||
bs_1 = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and write the correct word for each space.",
|
||||
"questions": None,
|
||||
"type": "blankSpaceText",
|
||||
"part": 3
|
||||
}
|
||||
|
||||
bs_2 = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and write the correct word for each space.",
|
||||
"questions": None,
|
||||
"type": "blankSpaceText",
|
||||
"part": 4
|
||||
}
|
||||
|
||||
reading = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Read the text and answer the questions below.",
|
||||
"questions": None,
|
||||
"type": "readingExercises",
|
||||
"part": 5
|
||||
}
|
||||
|
||||
all_mc_questions = []
|
||||
|
||||
# PART 1
|
||||
# await self._gen_multiple_choice("normal", number_of_exercises, utas=False)
|
||||
mc_exercises1 = await self.gen_multiple_choice(
|
||||
"blank_space", 15, 1, utas=True, all_exams=all_mc_questions
|
||||
)
|
||||
print(json.dumps(mc_exercises1, indent=4))
|
||||
all_mc_questions.append(mc_exercises1)
|
||||
|
||||
# PART 2
|
||||
mc_exercises2 = await self.gen_multiple_choice(
|
||||
"blank_space", 15, 16, utas=True, all_exams=all_mc_questions
|
||||
)
|
||||
print(json.dumps(mc_exercises2, indent=4))
|
||||
all_mc_questions.append(mc_exercises2)
|
||||
|
||||
# PART 3
|
||||
mc_exercises3 = await self.gen_multiple_choice(
|
||||
"blank_space", 15, 31, utas=True, all_exams=all_mc_questions
|
||||
)
|
||||
print(json.dumps(mc_exercises3, indent=4))
|
||||
all_mc_questions.append(mc_exercises3)
|
||||
|
||||
mc_exercises = mc_exercises1['questions'] + mc_exercises2['questions'] + mc_exercises3['questions']
|
||||
print(json.dumps(mc_exercises, indent=4))
|
||||
mc["questions"] = mc_exercises
|
||||
|
||||
# Underlined mc
|
||||
underlined_mc = await self.gen_multiple_choice(
|
||||
"underline", 15, 46, utas=True, all_exams=all_mc_questions
|
||||
)
|
||||
print(json.dumps(underlined_mc, indent=4))
|
||||
umc["questions"] = underlined_mc
|
||||
|
||||
# Blank Space text 1
|
||||
blank_space_text_1 = await self.gen_blank_space_text_utas(12, 61, 250)
|
||||
print(json.dumps(blank_space_text_1, indent=4))
|
||||
bs_1["questions"] = blank_space_text_1
|
||||
|
||||
# Blank Space text 2
|
||||
blank_space_text_2 = await self.gen_blank_space_text_utas(14, 73, 350)
|
||||
print(json.dumps(blank_space_text_2, indent=4))
|
||||
bs_2["questions"] = blank_space_text_2
|
||||
|
||||
# Reading text
|
||||
reading_text = await self.gen_reading_passage_utas(87, 10, 4)
|
||||
print(json.dumps(reading_text, indent=4))
|
||||
reading["questions"] = reading_text
|
||||
|
||||
return {
|
||||
"exercises": {
|
||||
"blankSpaceMultipleChoice": mc,
|
||||
"underlinedMultipleChoice": umc,
|
||||
"blankSpaceText1": bs_1,
|
||||
"blankSpaceText2": bs_2,
|
||||
"readingExercises": reading,
|
||||
},
|
||||
"isDiagnostic": diagnostic,
|
||||
"minTimer": min_timer,
|
||||
"module": "level"
|
||||
}
|
||||
|
||||
async def gen_multiple_choice(
|
||||
self, mc_variant: str, quantity: int, start_id: int = 1, *, utas: bool = False, all_exams=None
|
||||
):
|
||||
mc_template = self._mc_variants[mc_variant]
|
||||
blank_mod = " blank space " if mc_variant == "blank_space" else " "
|
||||
|
||||
gen_multiple_choice_for_text: str = (
|
||||
'Generate {quantity} multiple choice{blank}questions of 4 options for an english level exam, some easy '
|
||||
'questions, some intermediate questions and some advanced questions. Ensure that the questions cover '
|
||||
'a range of topics such as verb tense, subject-verb agreement, pronoun usage, sentence structure, and '
|
||||
'punctuation. Make sure every question only has 1 correct answer.'
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f'You are a helpful assistant designed to output JSON on this format: {mc_template}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": gen_multiple_choice_for_text.format(quantity=str(quantity), blank=blank_mod)
|
||||
}
|
||||
]
|
||||
|
||||
if mc_variant == "underline":
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
'The type of multiple choice in the prompt has wrong words or group of words and the options '
|
||||
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
|
||||
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
|
||||
'the boss <u>is</u> nice."\n'
|
||||
'Options:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
|
||||
)
|
||||
})
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if len(question["questions"]) != quantity:
|
||||
return await self.gen_multiple_choice(mc_variant, quantity, start_id, utas=utas, all_exams=all_exams)
|
||||
else:
|
||||
if not utas:
|
||||
all_exams = await self._document_store.get_all("level")
|
||||
seen_keys = set()
|
||||
for i in range(len(question["questions"])):
|
||||
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
|
||||
all_exams, question["questions"][i], question, seen_keys, mc_variant, utas
|
||||
)
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"prompt": "Select the appropriate option.",
|
||||
"questions": ExercisesHelper.fix_exercise_ids(question, start_id)["questions"],
|
||||
"type": "multipleChoice",
|
||||
}
|
||||
else:
|
||||
if all_exams is not None:
|
||||
seen_keys = set()
|
||||
for i in range(len(question["questions"])):
|
||||
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
|
||||
all_exams, question["questions"][i], question, seen_keys, mc_variant, utas
|
||||
)
|
||||
response = ExercisesHelper.fix_exercise_ids(question, start_id)
|
||||
response["questions"] = ExercisesHelper.randomize_mc_options_order(response["questions"])
|
||||
return response
|
||||
|
||||
async def _generate_single_multiple_choice(self, mc_variant: str = "normal"):
|
||||
mc_template = self._mc_variants[mc_variant]["questions"][0]
|
||||
blank_mod = " blank space " if mc_variant == "blank_space" else " "
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f'You are a helpful assistant designed to output JSON on this format: {mc_template}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate 1 multiple choice {blank_mod} question of 4 options for an english level exam, '
|
||||
f'it can be easy, intermediate or advanced.'
|
||||
)
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
if mc_variant == "underline":
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
'The type of multiple choice in the prompt has wrong words or group of words and the options '
|
||||
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
|
||||
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
|
||||
'the boss <u>is</u> nice."\n'
|
||||
'Options:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
|
||||
)
|
||||
})
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["options"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return question
|
||||
|
||||
async def _replace_exercise_if_exists(
|
||||
self, all_exams, current_exercise, current_exam, seen_keys, mc_variant: str, utas: bool = False
|
||||
):
|
||||
# Extracting relevant fields for comparison
|
||||
key = (current_exercise['prompt'], tuple(sorted(option['text'] for option in current_exercise['options'])))
|
||||
# Check if the key is in the set
|
||||
if key in seen_keys:
|
||||
return await self._replace_exercise_if_exists(
|
||||
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam, seen_keys,
|
||||
mc_variant, utas
|
||||
)
|
||||
else:
|
||||
seen_keys.add(key)
|
||||
|
||||
if not utas:
|
||||
for exam in all_exams:
|
||||
exam_dict = exam.to_dict()
|
||||
if len(exam_dict.get("parts", [])) > 0:
|
||||
exercise_dict = exam_dict.get("parts", [])[0]
|
||||
if len(exercise_dict.get("exercises", [])) > 0:
|
||||
if any(
|
||||
exercise["prompt"] == current_exercise["prompt"] and
|
||||
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
|
||||
current_exercise["options"])
|
||||
for exercise in exercise_dict.get("exercises", [])[0]["questions"]
|
||||
):
|
||||
return await self._replace_exercise_if_exists(
|
||||
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam,
|
||||
seen_keys, mc_variant, utas
|
||||
)
|
||||
else:
|
||||
for exam in all_exams:
|
||||
if any(
|
||||
exercise["prompt"] == current_exercise["prompt"] and
|
||||
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
|
||||
current_exercise["options"])
|
||||
for exercise in exam.get("questions", [])
|
||||
):
|
||||
return await self._replace_exercise_if_exists(
|
||||
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam,
|
||||
seen_keys, mc_variant, utas
|
||||
)
|
||||
return current_exercise, seen_keys
|
||||
|
||||
async def gen_blank_space_text_utas(
|
||||
self, quantity: int, start_id: int, size: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
json_template = self._mc_variants["blank_space_text"]
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_template}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Generate a text of at least {size} words about the topic {topic}.'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'From the generated text choose {quantity} words (cannot be sequential words) to replace '
|
||||
'once with {{id}} where id starts on ' + str(start_id) + ' and is incremented for each word. '
|
||||
'The ids must be ordered throughout the text and the words must be replaced only once. '
|
||||
'Put the removed words and respective ids on the words array of the json in the correct order.'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["question"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return question["question"]
|
||||
|
||||
async def gen_reading_passage_utas(
|
||||
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
|
||||
):
|
||||
passage = await self._reading_service.generate_reading_passage(1, topic)
|
||||
short_answer = await self._gen_short_answer_utas(passage["text"], start_id, sa_quantity)
|
||||
mc_exercises = await self._gen_text_multiple_choice_utas(passage["text"], start_id + sa_quantity, mc_quantity)
|
||||
return {
|
||||
"exercises": {
|
||||
"shortAnswer": short_answer,
|
||||
"multipleChoice": mc_exercises,
|
||||
},
|
||||
"text": {
|
||||
"content": passage["text"],
|
||||
"title": passage["title"]
|
||||
}
|
||||
}
|
||||
|
||||
async def _gen_short_answer_utas(self, text: str, start_id: int, sa_quantity: int):
|
||||
json_format = {"questions": [{"id": 1, "question": "question", "possible_answers": ["answer_1", "answer_2"]}]}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_format}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Generate {sa_quantity} short answer questions, and the possible answers, must have '
|
||||
f'maximum 3 words per answer, about this text:\n"{text}"'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'The id starts at {start_id}.'
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
return question["questions"]
|
||||
|
||||
async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int):
|
||||
json_template = self._mc_variants["text_mc_utas"]
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f'You are a helpful assistant designed to output JSON on this format: {json_template}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Generate {mc_quantity} multiple choice questions of 4 options for this text:\n{text}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": 'Make sure every question only has 1 correct answer.'
|
||||
}
|
||||
]
|
||||
|
||||
question = await self._llm.prediction(
|
||||
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
if len(question["questions"]) != mc_quantity:
|
||||
return await self._gen_text_multiple_choice_utas(text, mc_quantity, start_id)
|
||||
else:
|
||||
response = ExercisesHelper.fix_exercise_ids(question, start_id)
|
||||
response["questions"] = ExercisesHelper.randomize_mc_options_order(response["questions"])
|
||||
return response
|
||||
@@ -1,137 +1,137 @@
|
||||
{
|
||||
"normal": {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "And"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "Cat"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "Happy"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "Jump"
|
||||
}
|
||||
],
|
||||
"prompt": "Which of the following is a conjunction?",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"blank_space": {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "And"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "Cat"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "Happy"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "Jump"
|
||||
}
|
||||
],
|
||||
"prompt": "Which of the following is a conjunction?",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"underline": {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"prompt": "prompt",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"blank_space_text": {
|
||||
"question": {
|
||||
"words": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"text": "text"
|
||||
}
|
||||
},
|
||||
"text_mc_utas": {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"prompt": "prompt",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"normal": {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "And"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "Cat"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "Happy"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "Jump"
|
||||
}
|
||||
],
|
||||
"prompt": "Which of the following is a conjunction?",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"blank_space": {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "And"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "Cat"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "Happy"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "Jump"
|
||||
}
|
||||
],
|
||||
"prompt": "Which of the following is a conjunction?",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"underline": {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"prompt": "prompt",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"blank_space_text": {
|
||||
"question": {
|
||||
"words": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"text": "text"
|
||||
}
|
||||
},
|
||||
"text_mc_utas": {
|
||||
"questions": [
|
||||
{
|
||||
"id": "9",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "d"
|
||||
}
|
||||
],
|
||||
"prompt": "prompt",
|
||||
"solution": "A",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,404 +1,404 @@
|
||||
import aiofiles
|
||||
import os
|
||||
import uuid
|
||||
from logging import getLogger
|
||||
|
||||
from typing import Dict, Any, Tuple, 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.dtos.exam import Exam
|
||||
from app.dtos.sheet import Sheet
|
||||
|
||||
|
||||
class UploadLevelModule:
|
||||
def __init__(self, openai: ILLMService):
|
||||
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)
|
||||
FileHelper.convert_file_to_pdf(
|
||||
f'./tmp/{path_id}/uploaded.{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')
|
||||
|
||||
completion: Coroutine[Any, Any, Exam] = (
|
||||
self._png_completion(path_id) if file_has_images else self._html_completion(path_id)
|
||||
)
|
||||
response = await completion
|
||||
|
||||
FileHelper.remove_directory(f'./tmp/{path_id}')
|
||||
|
||||
if response:
|
||||
return self.fix_ids(response.dict(exclude_none=True))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@LoggerHelper.suppress_loggers()
|
||||
def _check_pdf_for_images(pdf_path: str) -> bool:
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
for page in pdf.pages:
|
||||
if page.images:
|
||||
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": [
|
||||
{
|
||||
"context": "<this attribute is optional you may exclude it if not required>",
|
||||
"exercises": [
|
||||
self._multiple_choice_html(),
|
||||
self._passage_blank_space_html()
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async def _html_completion(self, path_id: str) -> Exam:
|
||||
async with aiofiles.open(f'./tmp/{path_id}/exercises.html', 'r', encoding='utf-8') as f:
|
||||
html = await f.read()
|
||||
|
||||
return await self._llm.pydantic_prediction(
|
||||
[self._gpt_instructions_html(),
|
||||
{
|
||||
"role": "user",
|
||||
"content": html
|
||||
}
|
||||
],
|
||||
ExamMapper.map_to_exam_model,
|
||||
str(self._level_json_schema())
|
||||
)
|
||||
|
||||
def _gpt_instructions_html(self):
|
||||
return {
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are GPT Scraper and your job is to clean dirty html into clean usable JSON formatted data.'
|
||||
'Your current task is to scrape html english questions sheets.\n\n'
|
||||
|
||||
'In the question sheet you will only see 4 types of question:\n'
|
||||
'- blank space multiple choice\n'
|
||||
'- underline multiple choice\n'
|
||||
'- reading passage blank space multiple choice\n'
|
||||
'- reading passage multiple choice\n\n'
|
||||
|
||||
'For the first two types of questions the template is the same but the question prompts differ, '
|
||||
'whilst in the blank space multiple choice you must include in the prompt the blank spaces with '
|
||||
'multiple "_", in the underline you must include in the prompt the <u></u> to '
|
||||
'indicate the underline and the options a, b, c, d must be the ordered underlines in the prompt.\n\n'
|
||||
|
||||
'For the reading passage exercise you must handle the formatting of the passages. If it is a '
|
||||
'reading passage with blank spaces you will see blanks represented with (question id) followed by a '
|
||||
'line and your job is to replace the brackets with the question id and line with "{{question id}}" '
|
||||
'with 2 newlines between paragraphs. For the reading passages without blanks you must remove '
|
||||
'any numbers that may be there to specify paragraph numbers or line numbers, and place 2 newlines '
|
||||
'between paragraphs.\n\n'
|
||||
|
||||
'IMPORTANT: Note that for the reading passages, the html might not reflect the actual paragraph '
|
||||
'structure, don\'t format the reading passages paragraphs only by the <p></p> tags, try to figure '
|
||||
'out the best paragraph separation possible.'
|
||||
|
||||
'You will place all the information in a single JSON: '
|
||||
'{"parts": [{"exercises": [{...}], "context": ""}]}\n '
|
||||
'Where {...} are the exercises templates for each part of a question sheet and the optional field '
|
||||
'context.'
|
||||
|
||||
'IMPORTANT: The question sheet may be divided by sections but you need to only consider the parts, '
|
||||
'so that you can group the exercises by the parts that are in the html, this is crucial since only '
|
||||
'reading passage multiple choice require context and if the context is included in parts where it '
|
||||
'is not required the UI will be messed up. Some make sure to correctly group the exercises by parts.\n'
|
||||
|
||||
'The templates for the exercises are the following:\n'
|
||||
'- blank space multiple choice, underline multiple choice and reading passage multiple choice: '
|
||||
f'{self._multiple_choice_html()}\n'
|
||||
f'- reading passage blank space multiple choice: {self._passage_blank_space_html()}\n'
|
||||
|
||||
'IMPORTANT: For the reading passage multiple choice the context field must be set with the reading '
|
||||
'passages without paragraphs or line numbers, with 2 newlines between paragraphs, for the other '
|
||||
'exercises exclude the context field.'
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _multiple_choice_html():
|
||||
return {
|
||||
"type": "multipleChoice",
|
||||
"prompt": "Select the appropriate option.",
|
||||
"questions": [
|
||||
{
|
||||
"id": "<the question id>",
|
||||
"prompt": "<the question>",
|
||||
"solution": "<the option id solution>",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "<the a option>"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "<the b option>"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "<the c option>"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "<the d option>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _passage_blank_space_html():
|
||||
return {
|
||||
"type": "fillBlanks",
|
||||
"variant": "mc",
|
||||
"prompt": "Click a blank to select the appropriate word for it.",
|
||||
"text": (
|
||||
"<The whole text for the exercise with replacements for blank spaces and their "
|
||||
"ids with {{<question id>}} with 2 newlines between paragraphs>"
|
||||
),
|
||||
"solutions": [
|
||||
{
|
||||
"id": "<question id>",
|
||||
"solution": "<the option that holds the solution>"
|
||||
}
|
||||
],
|
||||
"words": [
|
||||
{
|
||||
"id": "<question id>",
|
||||
"options": {
|
||||
"A": "<a option>",
|
||||
"B": "<b option>",
|
||||
"C": "<c option>",
|
||||
"D": "<d option>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async def _png_completion(self, path_id: str) -> Exam:
|
||||
FileHelper.pdf_to_png(path_id)
|
||||
|
||||
tmp_files = os.listdir(f'./tmp/{path_id}')
|
||||
pages = [f for f in tmp_files if f.startswith('page-') and f.endswith('.png')]
|
||||
pages.sort(key=lambda f: int(f.split('-')[1].split('.')[0]))
|
||||
|
||||
json_schema = {
|
||||
"components": [
|
||||
{"type": "part", "part": "<name or number of the part>"},
|
||||
self._multiple_choice_png(),
|
||||
{"type": "blanksPassage", "text": (
|
||||
"<The whole text for the exercise with replacements for blank spaces and their "
|
||||
"ids with {{<question id>}} with 2 newlines between paragraphs>"
|
||||
)},
|
||||
{"type": "passage", "context": (
|
||||
"<reading passages without paragraphs or line numbers, with 2 newlines between paragraphs>"
|
||||
)},
|
||||
self._passage_blank_space_png()
|
||||
]
|
||||
}
|
||||
|
||||
components = []
|
||||
|
||||
for i in range(len(pages)):
|
||||
current_page = pages[i]
|
||||
next_page = pages[i + 1] if i + 1 < len(pages) else None
|
||||
batch = [current_page, next_page] if next_page else [current_page]
|
||||
|
||||
sheet = await self._png_batch(path_id, batch, json_schema)
|
||||
sheet.batch = i + 1
|
||||
components.append(sheet.dict())
|
||||
|
||||
batches = {"batches": components}
|
||||
|
||||
return await self._batches_to_exam_completion(batches)
|
||||
|
||||
async def _png_batch(self, path_id: str, files: list[str], json_schema) -> Sheet:
|
||||
return await self._llm.pydantic_prediction(
|
||||
[self._gpt_instructions_png(),
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
*FileHelper.b64_pngs(path_id, files)
|
||||
]
|
||||
}
|
||||
],
|
||||
ExamMapper.map_to_sheet,
|
||||
str(json_schema)
|
||||
)
|
||||
|
||||
def _gpt_instructions_png(self):
|
||||
return {
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are GPT OCR and your job is to scan image text data and format it to JSON format.'
|
||||
'Your current task is to scan english questions sheets.\n\n'
|
||||
|
||||
'You will place all the information in a single JSON: {"components": [{...}]} where {...} is a set of '
|
||||
'sheet components you will retrieve from the images, the components and their corresponding JSON '
|
||||
'templates are as follows:\n'
|
||||
|
||||
'- Part, a standalone part or part of a section of the question sheet: '
|
||||
'{"type": "part", "part": "<name or number of the part>"}\n'
|
||||
|
||||
'- Multiple Choice Question, there are three types of multiple choice questions that differ on '
|
||||
'the prompt field of the template: blanks, underlines and normal. '
|
||||
|
||||
'In the blanks prompt you must leave 5 underscores to represent the blank space. '
|
||||
'In the underlines questions the objective is to pick the words that are incorrect in the given '
|
||||
'sentence, for these questions you must wrap the answer to the question with the html tag <u></u>, '
|
||||
'choose 3 other words to wrap in <u></u>, place them in the prompt field and use the underlined words '
|
||||
'in the order they appear in the question for the options A to D, disreguard options that might be '
|
||||
'included underneath the underlines question and use the ones you wrapped in <u></u>.'
|
||||
'In normal you just leave the question as is. '
|
||||
|
||||
f'The template for multiple choice questions is the following: {self._multiple_choice_png()}.\n'
|
||||
|
||||
'- Reading Passages, there are two types of reading passages. Reading passages where you will see '
|
||||
'blanks represented by a (question id) followed by a line, you must format these types of reading '
|
||||
'passages to be only the text with the brackets that have the question id and line replaced with '
|
||||
'"{{question id}}", also place 2 newlines between paragraphs. For the reading passages without blanks '
|
||||
'you must remove any numbers that may be there to specify paragraph numbers or line numbers, '
|
||||
'and place 2 newlines between paragraphs. '
|
||||
|
||||
'For the reading passages with blanks the template is: {"type": "blanksPassage", '
|
||||
'"text": "<The whole text for the exercise with replacements for blank spaces and their '
|
||||
'ids that are enclosed in brackets with {{<question id>}} also place 2 newlines between paragraphs>"}. '
|
||||
|
||||
'For the reading passage without blanks is: {"type": "passage", "context": "<reading passages without '
|
||||
'paragraphs or line numbers, with 2 newlines between paragraphs>"}\n'
|
||||
|
||||
'- 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'
|
||||
|
||||
'IMPORTANT: You must place the components in the order that they were given to you. If an exercise or '
|
||||
'reading passages are cut off don\'t include them in the JSON.'
|
||||
)
|
||||
}
|
||||
|
||||
def _multiple_choice_png(self):
|
||||
multiple_choice = self._multiple_choice_html()["questions"][0]
|
||||
multiple_choice["type"] = "multipleChoice"
|
||||
multiple_choice.pop("solution")
|
||||
return multiple_choice
|
||||
|
||||
def _passage_blank_space_png(self):
|
||||
passage_blank_space = self._passage_blank_space_html()["words"][0]
|
||||
passage_blank_space["type"] = "fillBlanks"
|
||||
return passage_blank_space
|
||||
|
||||
async def _batches_to_exam_completion(self, batches: Dict[str, Any]) -> Exam:
|
||||
return await self._llm.pydantic_prediction(
|
||||
[self._gpt_instructions_html(),
|
||||
{
|
||||
"role": "user",
|
||||
"content": str(batches)
|
||||
}
|
||||
],
|
||||
ExamMapper.map_to_exam_model,
|
||||
str(self._level_json_schema())
|
||||
)
|
||||
|
||||
def _gpt_instructions_batches(self):
|
||||
return {
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are helpfull assistant. Your task is to merge multiple batches of english question sheet '
|
||||
'components and solve the questions. Each batch may contain overlapping content with the previous '
|
||||
'batch, or close enough content which needs to be excluded. The components are as follows:'
|
||||
|
||||
'- Part, a standalone part or part of a section of the question sheet: '
|
||||
'{"type": "part", "part": "<name or number of the part>"}\n'
|
||||
|
||||
'- Multiple Choice Question, there are three types of multiple choice questions that differ on '
|
||||
'the prompt field of the template: blanks, underlines and normal. '
|
||||
|
||||
'In a blanks question, the prompt has underscores to represent the blank space, you must select the '
|
||||
'appropriate option to solve it.'
|
||||
|
||||
'In a underlines question, the prompt has 4 underlines represented by the html tags <u></u>, you must '
|
||||
'select the option that makes the prompt incorrect to solve it. If the options order doesn\'t reflect '
|
||||
'the order in which the underlines appear in the prompt you will need to fix it.'
|
||||
|
||||
'In a normal question there isn\'t either blanks or underlines in the prompt, you should just '
|
||||
'select the appropriate solution.'
|
||||
|
||||
f'The template for these questions is the same: {self._multiple_choice_png()}\n'
|
||||
|
||||
'- Reading Passages, there are two types of reading passages with different templates. The one with '
|
||||
'type "blanksPassage" where the text field holds the passage and a blank is represented by '
|
||||
'{{<some number>}} and the other one with type "passage" that has the context field with just '
|
||||
'reading passages. For both of these components you will have to remove any additional data that might '
|
||||
'be related to a question description and also remove some "(<question id>)" and "_" from blanksPassage'
|
||||
' if there are any. These components are used in conjunction with other ones.'
|
||||
|
||||
'- Blanks Options, options for a blanks reading passage exercise, this type of component is a group of '
|
||||
'options with the question id and the options from a to d. The template is: '
|
||||
f'{self._passage_blank_space_png()}\n\n'
|
||||
|
||||
'Now that you know the possible components here\'s what I want you to do:\n'
|
||||
'1. Remove duplicates. A batch will have duplicates of other batches and the components of '
|
||||
'the next batch should always take precedence over the previous one batch, what I mean by this is that '
|
||||
'if batch 1 has, for example, multiple choice question with id 10 and the next one also has id 10, '
|
||||
'you pick the next one.\n'
|
||||
'2. Solve the exercises. There are 4 types of exercises, the 3 multipleChoice variants + a fill blanks '
|
||||
'exercise. For the multiple choice question follow the previous instruction to solve them and place '
|
||||
f'them in this format: {self._multiple_choice_html()}. For the fill blanks exercises you need to match '
|
||||
'the correct blanksPassage to the correct fillBlanks options and then pick the correct option. Here is '
|
||||
f'the template for this exercise: {self._passage_blank_space_html()}.\n'
|
||||
f'3. Restructure the JSON to match this template: {self._level_json_schema()}. '
|
||||
f'You must group the exercises by the parts in the order they appear in the batches components. '
|
||||
f'The context field of a part is the context of a passage component that has text relevant to normal '
|
||||
f'multiple choice questions.\n'
|
||||
|
||||
'Do your utmost to fullfill the requisites, make sure you include all non-duplicate questions'
|
||||
'in your response and correctly structure the JSON.'
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fix_ids(response):
|
||||
counter = 1
|
||||
for part in response["parts"]:
|
||||
for exercise in part["exercises"]:
|
||||
if exercise["type"] == "multipleChoice":
|
||||
for question in exercise["questions"]:
|
||||
question["id"] = counter
|
||||
counter += 1
|
||||
if exercise["type"] == "fillBlanks":
|
||||
for i in range(len(exercise["words"])):
|
||||
exercise["words"][i]["id"] = counter
|
||||
exercise["solutions"][i]["id"] = counter
|
||||
counter += 1
|
||||
return response
|
||||
import aiofiles
|
||||
import os
|
||||
import uuid
|
||||
from logging import getLogger
|
||||
|
||||
from typing import Dict, Any, Tuple, 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.dtos.exam import Exam
|
||||
from app.dtos.sheet import Sheet
|
||||
|
||||
|
||||
class UploadLevelModule:
|
||||
def __init__(self, openai: ILLMService):
|
||||
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)
|
||||
FileHelper.convert_file_to_pdf(
|
||||
f'./tmp/{path_id}/uploaded.{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')
|
||||
|
||||
completion: Coroutine[Any, Any, Exam] = (
|
||||
self._png_completion(path_id) if file_has_images else self._html_completion(path_id)
|
||||
)
|
||||
response = await completion
|
||||
|
||||
FileHelper.remove_directory(f'./tmp/{path_id}')
|
||||
|
||||
if response:
|
||||
return self.fix_ids(response.dict(exclude_none=True))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@LoggerHelper.suppress_loggers()
|
||||
def _check_pdf_for_images(pdf_path: str) -> bool:
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
for page in pdf.pages:
|
||||
if page.images:
|
||||
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": [
|
||||
{
|
||||
"context": "<this attribute is optional you may exclude it if not required>",
|
||||
"exercises": [
|
||||
self._multiple_choice_html(),
|
||||
self._passage_blank_space_html()
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async def _html_completion(self, path_id: str) -> Exam:
|
||||
async with aiofiles.open(f'./tmp/{path_id}/exercises.html', 'r', encoding='utf-8') as f:
|
||||
html = await f.read()
|
||||
|
||||
return await self._llm.pydantic_prediction(
|
||||
[self._gpt_instructions_html(),
|
||||
{
|
||||
"role": "user",
|
||||
"content": html
|
||||
}
|
||||
],
|
||||
ExamMapper.map_to_exam_model,
|
||||
str(self._level_json_schema())
|
||||
)
|
||||
|
||||
def _gpt_instructions_html(self):
|
||||
return {
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are GPT Scraper and your job is to clean dirty html into clean usable JSON formatted data.'
|
||||
'Your current task is to scrape html english questions sheets.\n\n'
|
||||
|
||||
'In the question sheet you will only see 4 types of question:\n'
|
||||
'- blank space multiple choice\n'
|
||||
'- underline multiple choice\n'
|
||||
'- reading passage blank space multiple choice\n'
|
||||
'- reading passage multiple choice\n\n'
|
||||
|
||||
'For the first two types of questions the template is the same but the question prompts differ, '
|
||||
'whilst in the blank space multiple choice you must include in the prompt the blank spaces with '
|
||||
'multiple "_", in the underline you must include in the prompt the <u></u> to '
|
||||
'indicate the underline and the options a, b, c, d must be the ordered underlines in the prompt.\n\n'
|
||||
|
||||
'For the reading passage exercise you must handle the formatting of the passages. If it is a '
|
||||
'reading passage with blank spaces you will see blanks represented with (question id) followed by a '
|
||||
'line and your job is to replace the brackets with the question id and line with "{{question id}}" '
|
||||
'with 2 newlines between paragraphs. For the reading passages without blanks you must remove '
|
||||
'any numbers that may be there to specify paragraph numbers or line numbers, and place 2 newlines '
|
||||
'between paragraphs.\n\n'
|
||||
|
||||
'IMPORTANT: Note that for the reading passages, the html might not reflect the actual paragraph '
|
||||
'structure, don\'t format the reading passages paragraphs only by the <p></p> tags, try to figure '
|
||||
'out the best paragraph separation possible.'
|
||||
|
||||
'You will place all the information in a single JSON: '
|
||||
'{"parts": [{"exercises": [{...}], "context": ""}]}\n '
|
||||
'Where {...} are the exercises templates for each part of a question sheet and the optional field '
|
||||
'context.'
|
||||
|
||||
'IMPORTANT: The question sheet may be divided by sections but you need to only consider the parts, '
|
||||
'so that you can group the exercises by the parts that are in the html, this is crucial since only '
|
||||
'reading passage multiple choice require context and if the context is included in parts where it '
|
||||
'is not required the UI will be messed up. Some make sure to correctly group the exercises by parts.\n'
|
||||
|
||||
'The templates for the exercises are the following:\n'
|
||||
'- blank space multiple choice, underline multiple choice and reading passage multiple choice: '
|
||||
f'{self._multiple_choice_html()}\n'
|
||||
f'- reading passage blank space multiple choice: {self._passage_blank_space_html()}\n'
|
||||
|
||||
'IMPORTANT: For the reading passage multiple choice the context field must be set with the reading '
|
||||
'passages without paragraphs or line numbers, with 2 newlines between paragraphs, for the other '
|
||||
'exercises exclude the context field.'
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _multiple_choice_html():
|
||||
return {
|
||||
"type": "multipleChoice",
|
||||
"prompt": "Select the appropriate option.",
|
||||
"questions": [
|
||||
{
|
||||
"id": "<the question id>",
|
||||
"prompt": "<the question>",
|
||||
"solution": "<the option id solution>",
|
||||
"options": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "<the a option>"
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"text": "<the b option>"
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"text": "<the c option>"
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"text": "<the d option>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _passage_blank_space_html():
|
||||
return {
|
||||
"type": "fillBlanks",
|
||||
"variant": "mc",
|
||||
"prompt": "Click a blank to select the appropriate word for it.",
|
||||
"text": (
|
||||
"<The whole text for the exercise with replacements for blank spaces and their "
|
||||
"ids with {{<question id>}} with 2 newlines between paragraphs>"
|
||||
),
|
||||
"solutions": [
|
||||
{
|
||||
"id": "<question id>",
|
||||
"solution": "<the option that holds the solution>"
|
||||
}
|
||||
],
|
||||
"words": [
|
||||
{
|
||||
"id": "<question id>",
|
||||
"options": {
|
||||
"A": "<a option>",
|
||||
"B": "<b option>",
|
||||
"C": "<c option>",
|
||||
"D": "<d option>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async def _png_completion(self, path_id: str) -> Exam:
|
||||
FileHelper.pdf_to_png(path_id)
|
||||
|
||||
tmp_files = os.listdir(f'./tmp/{path_id}')
|
||||
pages = [f for f in tmp_files if f.startswith('page-') and f.endswith('.png')]
|
||||
pages.sort(key=lambda f: int(f.split('-')[1].split('.')[0]))
|
||||
|
||||
json_schema = {
|
||||
"components": [
|
||||
{"type": "part", "part": "<name or number of the part>"},
|
||||
self._multiple_choice_png(),
|
||||
{"type": "blanksPassage", "text": (
|
||||
"<The whole text for the exercise with replacements for blank spaces and their "
|
||||
"ids with {{<question id>}} with 2 newlines between paragraphs>"
|
||||
)},
|
||||
{"type": "passage", "context": (
|
||||
"<reading passages without paragraphs or line numbers, with 2 newlines between paragraphs>"
|
||||
)},
|
||||
self._passage_blank_space_png()
|
||||
]
|
||||
}
|
||||
|
||||
components = []
|
||||
|
||||
for i in range(len(pages)):
|
||||
current_page = pages[i]
|
||||
next_page = pages[i + 1] if i + 1 < len(pages) else None
|
||||
batch = [current_page, next_page] if next_page else [current_page]
|
||||
|
||||
sheet = await self._png_batch(path_id, batch, json_schema)
|
||||
sheet.batch = i + 1
|
||||
components.append(sheet.dict())
|
||||
|
||||
batches = {"batches": components}
|
||||
|
||||
return await self._batches_to_exam_completion(batches)
|
||||
|
||||
async def _png_batch(self, path_id: str, files: list[str], json_schema) -> Sheet:
|
||||
return await self._llm.pydantic_prediction(
|
||||
[self._gpt_instructions_png(),
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
*FileHelper.b64_pngs(path_id, files)
|
||||
]
|
||||
}
|
||||
],
|
||||
ExamMapper.map_to_sheet,
|
||||
str(json_schema)
|
||||
)
|
||||
|
||||
def _gpt_instructions_png(self):
|
||||
return {
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are GPT OCR and your job is to scan image text data and format it to JSON format.'
|
||||
'Your current task is to scan english questions sheets.\n\n'
|
||||
|
||||
'You will place all the information in a single JSON: {"components": [{...}]} where {...} is a set of '
|
||||
'sheet components you will retrieve from the images, the components and their corresponding JSON '
|
||||
'templates are as follows:\n'
|
||||
|
||||
'- Part, a standalone part or part of a section of the question sheet: '
|
||||
'{"type": "part", "part": "<name or number of the part>"}\n'
|
||||
|
||||
'- Multiple Choice Question, there are three types of multiple choice questions that differ on '
|
||||
'the prompt field of the template: blanks, underlines and normal. '
|
||||
|
||||
'In the blanks prompt you must leave 5 underscores to represent the blank space. '
|
||||
'In the underlines questions the objective is to pick the words that are incorrect in the given '
|
||||
'sentence, for these questions you must wrap the answer to the question with the html tag <u></u>, '
|
||||
'choose 3 other words to wrap in <u></u>, place them in the prompt field and use the underlined words '
|
||||
'in the order they appear in the question for the options A to D, disreguard options that might be '
|
||||
'included underneath the underlines question and use the ones you wrapped in <u></u>.'
|
||||
'In normal you just leave the question as is. '
|
||||
|
||||
f'The template for multiple choice questions is the following: {self._multiple_choice_png()}.\n'
|
||||
|
||||
'- Reading Passages, there are two types of reading passages. Reading passages where you will see '
|
||||
'blanks represented by a (question id) followed by a line, you must format these types of reading '
|
||||
'passages to be only the text with the brackets that have the question id and line replaced with '
|
||||
'"{{question id}}", also place 2 newlines between paragraphs. For the reading passages without blanks '
|
||||
'you must remove any numbers that may be there to specify paragraph numbers or line numbers, '
|
||||
'and place 2 newlines between paragraphs. '
|
||||
|
||||
'For the reading passages with blanks the template is: {"type": "blanksPassage", '
|
||||
'"text": "<The whole text for the exercise with replacements for blank spaces and their '
|
||||
'ids that are enclosed in brackets with {{<question id>}} also place 2 newlines between paragraphs>"}. '
|
||||
|
||||
'For the reading passage without blanks is: {"type": "passage", "context": "<reading passages without '
|
||||
'paragraphs or line numbers, with 2 newlines between paragraphs>"}\n'
|
||||
|
||||
'- 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'
|
||||
|
||||
'IMPORTANT: You must place the components in the order that they were given to you. If an exercise or '
|
||||
'reading passages are cut off don\'t include them in the JSON.'
|
||||
)
|
||||
}
|
||||
|
||||
def _multiple_choice_png(self):
|
||||
multiple_choice = self._multiple_choice_html()["questions"][0]
|
||||
multiple_choice["type"] = "multipleChoice"
|
||||
multiple_choice.pop("solution")
|
||||
return multiple_choice
|
||||
|
||||
def _passage_blank_space_png(self):
|
||||
passage_blank_space = self._passage_blank_space_html()["words"][0]
|
||||
passage_blank_space["type"] = "fillBlanks"
|
||||
return passage_blank_space
|
||||
|
||||
async def _batches_to_exam_completion(self, batches: Dict[str, Any]) -> Exam:
|
||||
return await self._llm.pydantic_prediction(
|
||||
[self._gpt_instructions_html(),
|
||||
{
|
||||
"role": "user",
|
||||
"content": str(batches)
|
||||
}
|
||||
],
|
||||
ExamMapper.map_to_exam_model,
|
||||
str(self._level_json_schema())
|
||||
)
|
||||
|
||||
def _gpt_instructions_batches(self):
|
||||
return {
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are helpfull assistant. Your task is to merge multiple batches of english question sheet '
|
||||
'components and solve the questions. Each batch may contain overlapping content with the previous '
|
||||
'batch, or close enough content which needs to be excluded. The components are as follows:'
|
||||
|
||||
'- Part, a standalone part or part of a section of the question sheet: '
|
||||
'{"type": "part", "part": "<name or number of the part>"}\n'
|
||||
|
||||
'- Multiple Choice Question, there are three types of multiple choice questions that differ on '
|
||||
'the prompt field of the template: blanks, underlines and normal. '
|
||||
|
||||
'In a blanks question, the prompt has underscores to represent the blank space, you must select the '
|
||||
'appropriate option to solve it.'
|
||||
|
||||
'In a underlines question, the prompt has 4 underlines represented by the html tags <u></u>, you must '
|
||||
'select the option that makes the prompt incorrect to solve it. If the options order doesn\'t reflect '
|
||||
'the order in which the underlines appear in the prompt you will need to fix it.'
|
||||
|
||||
'In a normal question there isn\'t either blanks or underlines in the prompt, you should just '
|
||||
'select the appropriate solution.'
|
||||
|
||||
f'The template for these questions is the same: {self._multiple_choice_png()}\n'
|
||||
|
||||
'- Reading Passages, there are two types of reading passages with different templates. The one with '
|
||||
'type "blanksPassage" where the text field holds the passage and a blank is represented by '
|
||||
'{{<some number>}} and the other one with type "passage" that has the context field with just '
|
||||
'reading passages. For both of these components you will have to remove any additional data that might '
|
||||
'be related to a question description and also remove some "(<question id>)" and "_" from blanksPassage'
|
||||
' if there are any. These components are used in conjunction with other ones.'
|
||||
|
||||
'- Blanks Options, options for a blanks reading passage exercise, this type of component is a group of '
|
||||
'options with the question id and the options from a to d. The template is: '
|
||||
f'{self._passage_blank_space_png()}\n\n'
|
||||
|
||||
'Now that you know the possible components here\'s what I want you to do:\n'
|
||||
'1. Remove duplicates. A batch will have duplicates of other batches and the components of '
|
||||
'the next batch should always take precedence over the previous one batch, what I mean by this is that '
|
||||
'if batch 1 has, for example, multiple choice question with id 10 and the next one also has id 10, '
|
||||
'you pick the next one.\n'
|
||||
'2. Solve the exercises. There are 4 types of exercises, the 3 multipleChoice variants + a fill blanks '
|
||||
'exercise. For the multiple choice question follow the previous instruction to solve them and place '
|
||||
f'them in this format: {self._multiple_choice_html()}. For the fill blanks exercises you need to match '
|
||||
'the correct blanksPassage to the correct fillBlanks options and then pick the correct option. Here is '
|
||||
f'the template for this exercise: {self._passage_blank_space_html()}.\n'
|
||||
f'3. Restructure the JSON to match this template: {self._level_json_schema()}. '
|
||||
f'You must group the exercises by the parts in the order they appear in the batches components. '
|
||||
f'The context field of a part is the context of a passage component that has text relevant to normal '
|
||||
f'multiple choice questions.\n'
|
||||
|
||||
'Do your utmost to fullfill the requisites, make sure you include all non-duplicate questions'
|
||||
'in your response and correctly structure the JSON.'
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fix_ids(response):
|
||||
counter = 1
|
||||
for part in response["parts"]:
|
||||
for exercise in part["exercises"]:
|
||||
if exercise["type"] == "multipleChoice":
|
||||
for question in exercise["questions"]:
|
||||
question["id"] = counter
|
||||
counter += 1
|
||||
if exercise["type"] == "fillBlanks":
|
||||
for i in range(len(exercise["words"])):
|
||||
exercise["words"][i]["id"] = counter
|
||||
exercise["solutions"][i]["id"] = counter
|
||||
counter += 1
|
||||
return response
|
||||
@@ -1,492 +1,492 @@
|
||||
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}")
|
||||
|
||||
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)
|
||||
@@ -1,349 +1,349 @@
|
||||
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"
|
||||
}
|
||||
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"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,248 +1,248 @@
|
||||
from typing import List, Dict
|
||||
|
||||
from app.services.abc import IWritingService, ILLMService, IAIDetectorService
|
||||
from app.configs.constants import GPTModels, TemperatureSettings, FieldsAndExercises
|
||||
from app.helpers import TextHelper, ExercisesHelper
|
||||
|
||||
|
||||
class WritingService(IWritingService):
|
||||
|
||||
def __init__(self, llm: ILLMService, ai_detector: IAIDetectorService):
|
||||
self._llm = llm
|
||||
self._ai_detector = ai_detector
|
||||
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}'
|
||||
)
|
||||
},
|
||||
*self._get_writing_messages(task, topic, difficulty)
|
||||
]
|
||||
|
||||
llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O
|
||||
|
||||
response = await self._llm.prediction(
|
||||
llm_model,
|
||||
messages,
|
||||
["prompt"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
question = response["prompt"].strip()
|
||||
|
||||
return {
|
||||
"question": self._add_newline_before_hyphen(question) if task == 1 else question,
|
||||
"difficulty": difficulty,
|
||||
"topic": topic
|
||||
}
|
||||
|
||||
@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.'
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": task_prompt
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": task_instructions
|
||||
}
|
||||
]
|
||||
|
||||
return messages
|
||||
|
||||
async def grade_writing_task(self, task: int, question: str, answer: str):
|
||||
bare_minimum = 100 if task == 1 else 180
|
||||
|
||||
if not TextHelper.has_words(answer):
|
||||
return self._zero_rating("The answer does not contain enough english words.")
|
||||
elif not TextHelper.has_x_words(answer, bare_minimum):
|
||||
return self._zero_rating("The answer is insufficient and too small to be graded.")
|
||||
else:
|
||||
template = self._get_writing_template()
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f'You are a helpful assistant designed to output JSON on this format: {template}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Evaluate the given Writing Task {task} response based on the IELTS grading system, '
|
||||
'ensuring a strict assessment that penalizes errors. Deduct points for deviations '
|
||||
'from the task, and assign a score of 0 if the response fails to address the question. '
|
||||
'Additionally, provide a detailed commentary highlighting both strengths and '
|
||||
'weaknesses in the response. '
|
||||
f'\n Question: "{question}" \n Answer: "{answer}"')
|
||||
}
|
||||
]
|
||||
|
||||
if task == 1:
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Refer to the parts of the letter as: "Greeting Opener", "bullet 1", "bullet 2", '
|
||||
'"bullet 3", "closer (restate the purpose of the letter)", "closing greeting"'
|
||||
)
|
||||
})
|
||||
|
||||
llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O
|
||||
temperature = (
|
||||
TemperatureSettings.GRADING_TEMPERATURE
|
||||
if task == 1 else
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
response = await self._llm.prediction(
|
||||
llm_model,
|
||||
messages,
|
||||
["comment"],
|
||||
temperature
|
||||
)
|
||||
|
||||
perfect_answer_minimum = 150 if task == 1 else 250
|
||||
perfect_answer = await self._get_perfect_answer(question, perfect_answer_minimum)
|
||||
|
||||
response["perfect_answer"] = perfect_answer["perfect_answer"]
|
||||
response["overall"] = ExercisesHelper.fix_writing_overall(response["overall"], response["task_response"])
|
||||
response['fixed_text'] = await self._get_fixed_text(answer)
|
||||
|
||||
ai_detection = await self._ai_detector.run_detection(answer)
|
||||
if ai_detection is not None:
|
||||
response['ai_detection'] = ai_detection
|
||||
|
||||
return response
|
||||
|
||||
async def _get_fixed_text(self, text):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"fixed_text": "fixed test with no misspelling errors"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Fix the errors in the given text and put it in a JSON. '
|
||||
f'Do not complete the answer, only replace what is wrong. \n The text: "{text}"'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
messages,
|
||||
["fixed_text"],
|
||||
0.2,
|
||||
False
|
||||
)
|
||||
return response["fixed_text"]
|
||||
|
||||
async def _get_perfect_answer(self, question: str, size: int) -> Dict:
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"perfect_answer": "perfect answer for the question"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Write a perfect answer for this writing exercise of a IELTS exam. Question: {question}'
|
||||
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'The answer must have at least {size} words'
|
||||
}
|
||||
]
|
||||
return await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["perfect_answer"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _zero_rating(comment: str):
|
||||
return {
|
||||
'comment': comment,
|
||||
'overall': 0,
|
||||
'task_response': {
|
||||
'Task Achievement': {
|
||||
"grade": 0.0,
|
||||
"comment": ""
|
||||
},
|
||||
'Coherence and Cohesion': {
|
||||
"grade": 0.0,
|
||||
"comment": ""
|
||||
},
|
||||
'Lexical Resource': {
|
||||
"grade": 0.0,
|
||||
"comment": ""
|
||||
},
|
||||
'Grammatical Range and Accuracy': {
|
||||
"grade": 0.0,
|
||||
"comment": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_writing_template():
|
||||
return {
|
||||
"comment": "comment about student's response quality",
|
||||
"overall": 0.0,
|
||||
"task_response": {
|
||||
"Task Achievement": {
|
||||
"grade": 0.0,
|
||||
"comment": "comment about Task Achievement of the student's response"
|
||||
},
|
||||
"Coherence and Cohesion": {
|
||||
"grade": 0.0,
|
||||
"comment": "comment about Coherence and Cohesion of the student's response"
|
||||
},
|
||||
"Lexical Resource": {
|
||||
"grade": 0.0,
|
||||
"comment": "comment about Lexical Resource of the student's response"
|
||||
},
|
||||
"Grammatical Range and Accuracy": {
|
||||
"grade": 0.0,
|
||||
"comment": "comment about Grammatical Range and Accuracy of the student's response"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _add_newline_before_hyphen(s):
|
||||
return s.replace(" -", "\n-")
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
from app.services.abc import IWritingService, ILLMService, IAIDetectorService
|
||||
from app.configs.constants import GPTModels, TemperatureSettings, FieldsAndExercises
|
||||
from app.helpers import TextHelper, ExercisesHelper
|
||||
|
||||
|
||||
class WritingService(IWritingService):
|
||||
|
||||
def __init__(self, llm: ILLMService, ai_detector: IAIDetectorService):
|
||||
self._llm = llm
|
||||
self._ai_detector = ai_detector
|
||||
|
||||
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}'
|
||||
)
|
||||
},
|
||||
*self._get_writing_messages(task, topic, difficulty)
|
||||
]
|
||||
|
||||
llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O
|
||||
|
||||
response = await self._llm.prediction(
|
||||
llm_model,
|
||||
messages,
|
||||
["prompt"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
question = response["prompt"].strip()
|
||||
|
||||
return {
|
||||
"question": self._add_newline_before_hyphen(question) if task == 1 else question,
|
||||
"difficulty": difficulty,
|
||||
"topic": topic
|
||||
}
|
||||
|
||||
@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.'
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": task_prompt
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": task_instructions
|
||||
}
|
||||
]
|
||||
|
||||
return messages
|
||||
|
||||
async def grade_writing_task(self, task: int, question: str, answer: str):
|
||||
bare_minimum = 100 if task == 1 else 180
|
||||
|
||||
if not TextHelper.has_words(answer):
|
||||
return self._zero_rating("The answer does not contain enough english words.")
|
||||
elif not TextHelper.has_x_words(answer, bare_minimum):
|
||||
return self._zero_rating("The answer is insufficient and too small to be graded.")
|
||||
else:
|
||||
template = self._get_writing_template()
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f'You are a helpful assistant designed to output JSON on this format: {template}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f'Evaluate the given Writing Task {task} response based on the IELTS grading system, '
|
||||
'ensuring a strict assessment that penalizes errors. Deduct points for deviations '
|
||||
'from the task, and assign a score of 0 if the response fails to address the question. '
|
||||
'Additionally, provide a detailed commentary highlighting both strengths and '
|
||||
'weaknesses in the response. '
|
||||
f'\n Question: "{question}" \n Answer: "{answer}"')
|
||||
}
|
||||
]
|
||||
|
||||
if task == 1:
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Refer to the parts of the letter as: "Greeting Opener", "bullet 1", "bullet 2", '
|
||||
'"bullet 3", "closer (restate the purpose of the letter)", "closing greeting"'
|
||||
)
|
||||
})
|
||||
|
||||
llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O
|
||||
temperature = (
|
||||
TemperatureSettings.GRADING_TEMPERATURE
|
||||
if task == 1 else
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
response = await self._llm.prediction(
|
||||
llm_model,
|
||||
messages,
|
||||
["comment"],
|
||||
temperature
|
||||
)
|
||||
|
||||
perfect_answer_minimum = 150 if task == 1 else 250
|
||||
perfect_answer = await self._get_perfect_answer(question, perfect_answer_minimum)
|
||||
|
||||
response["perfect_answer"] = perfect_answer["perfect_answer"]
|
||||
response["overall"] = ExercisesHelper.fix_writing_overall(response["overall"], response["task_response"])
|
||||
response['fixed_text'] = await self._get_fixed_text(answer)
|
||||
|
||||
ai_detection = await self._ai_detector.run_detection(answer)
|
||||
if ai_detection is not None:
|
||||
response['ai_detection'] = ai_detection
|
||||
|
||||
return response
|
||||
|
||||
async def _get_fixed_text(self, text):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"fixed_text": "fixed test with no misspelling errors"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
'Fix the errors in the given text and put it in a JSON. '
|
||||
f'Do not complete the answer, only replace what is wrong. \n The text: "{text}"'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
messages,
|
||||
["fixed_text"],
|
||||
0.2,
|
||||
False
|
||||
)
|
||||
return response["fixed_text"]
|
||||
|
||||
async def _get_perfect_answer(self, question: str, size: int) -> Dict:
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
'You are a helpful assistant designed to output JSON on this format: '
|
||||
'{"perfect_answer": "perfect answer for the question"}'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Write a perfect answer for this writing exercise of a IELTS exam. Question: {question}'
|
||||
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'The answer must have at least {size} words'
|
||||
}
|
||||
]
|
||||
return await self._llm.prediction(
|
||||
GPTModels.GPT_4_O,
|
||||
messages,
|
||||
["perfect_answer"],
|
||||
TemperatureSettings.GEN_QUESTION_TEMPERATURE
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _zero_rating(comment: str):
|
||||
return {
|
||||
'comment': comment,
|
||||
'overall': 0,
|
||||
'task_response': {
|
||||
'Task Achievement': {
|
||||
"grade": 0.0,
|
||||
"comment": ""
|
||||
},
|
||||
'Coherence and Cohesion': {
|
||||
"grade": 0.0,
|
||||
"comment": ""
|
||||
},
|
||||
'Lexical Resource': {
|
||||
"grade": 0.0,
|
||||
"comment": ""
|
||||
},
|
||||
'Grammatical Range and Accuracy': {
|
||||
"grade": 0.0,
|
||||
"comment": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_writing_template():
|
||||
return {
|
||||
"comment": "comment about student's response quality",
|
||||
"overall": 0.0,
|
||||
"task_response": {
|
||||
"Task Achievement": {
|
||||
"grade": 0.0,
|
||||
"comment": "comment about Task Achievement of the student's response"
|
||||
},
|
||||
"Coherence and Cohesion": {
|
||||
"grade": 0.0,
|
||||
"comment": "comment about Coherence and Cohesion of the student's response"
|
||||
},
|
||||
"Lexical Resource": {
|
||||
"grade": 0.0,
|
||||
"comment": "comment about Lexical Resource of the student's response"
|
||||
},
|
||||
"Grammatical Range and Accuracy": {
|
||||
"grade": 0.0,
|
||||
"comment": "comment about Grammatical Range and Accuracy of the student's response"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _add_newline_before_hyphen(s):
|
||||
return s.replace(" -", "\n-")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from .aws_polly import AWSPolly
|
||||
from .heygen import Heygen
|
||||
from .openai import OpenAI
|
||||
from .whisper import OpenAIWhisper
|
||||
from .gpt_zero import GPTZero
|
||||
|
||||
__all__ = [
|
||||
"AWSPolly",
|
||||
"Heygen",
|
||||
"OpenAI",
|
||||
"OpenAIWhisper",
|
||||
"GPTZero"
|
||||
]
|
||||
from .aws_polly import AWSPolly
|
||||
from .heygen import Heygen
|
||||
from .openai import OpenAI
|
||||
from .whisper import OpenAIWhisper
|
||||
from .gpt_zero import GPTZero
|
||||
|
||||
__all__ = [
|
||||
"AWSPolly",
|
||||
"Heygen",
|
||||
"OpenAI",
|
||||
"OpenAIWhisper",
|
||||
"GPTZero"
|
||||
]
|
||||
|
||||
@@ -1,87 +1,87 @@
|
||||
import random
|
||||
from typing import Union
|
||||
|
||||
import aiofiles
|
||||
from aiobotocore.client import BaseClient
|
||||
|
||||
from app.services.abc import ITextToSpeechService
|
||||
from app.configs.constants import NeuralVoices
|
||||
|
||||
|
||||
class AWSPolly(ITextToSpeechService):
|
||||
|
||||
def __init__(self, client: BaseClient):
|
||||
self._client = client
|
||||
|
||||
async def synthesize_speech(self, text: str, voice: str, engine: str = "neural", output_format: str = "mp3"):
|
||||
tts_response = await self._client.synthesize_speech(
|
||||
Engine=engine,
|
||||
Text=text,
|
||||
OutputFormat=output_format,
|
||||
VoiceId=voice
|
||||
)
|
||||
return await tts_response['AudioStream'].read()
|
||||
|
||||
async def text_to_speech(self, text: Union[list[str], str], file_name: str):
|
||||
if isinstance(text, str):
|
||||
audio_segments = await self._text_to_speech(text)
|
||||
elif isinstance(text, list):
|
||||
audio_segments = await self._conversation_to_speech(text)
|
||||
else:
|
||||
raise ValueError("Unsupported argument for text_to_speech")
|
||||
|
||||
final_message = await self.synthesize_speech(
|
||||
"This audio recording, for the listening exercise, has finished.",
|
||||
"Stephen"
|
||||
)
|
||||
|
||||
# Add finish message
|
||||
audio_segments.append(final_message)
|
||||
|
||||
# Combine the audio segments into a single audio file
|
||||
combined_audio = b"".join(audio_segments)
|
||||
# Save the combined audio to a single file
|
||||
async with aiofiles.open(file_name, "wb") as f:
|
||||
await f.write(combined_audio)
|
||||
|
||||
print("Speech segments saved to " + file_name)
|
||||
|
||||
async def _text_to_speech(self, text: str):
|
||||
voice = random.choice(NeuralVoices.ALL_NEURAL_VOICES)['Id']
|
||||
# Initialize an empty list to store audio segments
|
||||
audio_segments = []
|
||||
for part in self._divide_text(text):
|
||||
audio_segments.append(await self.synthesize_speech(part, voice))
|
||||
|
||||
return audio_segments
|
||||
|
||||
async def _conversation_to_speech(self, conversation: list):
|
||||
# Initialize an empty list to store audio segments
|
||||
audio_segments = []
|
||||
# Iterate through the text segments, convert to audio segments, and store them
|
||||
for segment in conversation:
|
||||
audio_segments.append(await self.synthesize_speech(segment["text"], segment["voice"]))
|
||||
|
||||
return audio_segments
|
||||
|
||||
@staticmethod
|
||||
def _divide_text(text, max_length=3000):
|
||||
if len(text) <= max_length:
|
||||
return [text]
|
||||
|
||||
divisions = []
|
||||
current_position = 0
|
||||
|
||||
while current_position < len(text):
|
||||
next_position = min(current_position + max_length, len(text))
|
||||
next_period_position = text.rfind('.', current_position, next_position)
|
||||
|
||||
if next_period_position != -1 and next_period_position > current_position:
|
||||
divisions.append(text[current_position:next_period_position + 1])
|
||||
current_position = next_period_position + 1
|
||||
else:
|
||||
# If no '.' found in the next chunk, split at max_length
|
||||
divisions.append(text[current_position:next_position])
|
||||
current_position = next_position
|
||||
|
||||
return divisions
|
||||
import random
|
||||
from typing import Union
|
||||
|
||||
import aiofiles
|
||||
from aiobotocore.client import BaseClient
|
||||
|
||||
from app.services.abc import ITextToSpeechService
|
||||
from app.configs.constants import NeuralVoices
|
||||
|
||||
|
||||
class AWSPolly(ITextToSpeechService):
|
||||
|
||||
def __init__(self, client: BaseClient):
|
||||
self._client = client
|
||||
|
||||
async def synthesize_speech(self, text: str, voice: str, engine: str = "neural", output_format: str = "mp3"):
|
||||
tts_response = await self._client.synthesize_speech(
|
||||
Engine=engine,
|
||||
Text=text,
|
||||
OutputFormat=output_format,
|
||||
VoiceId=voice
|
||||
)
|
||||
return await tts_response['AudioStream'].read()
|
||||
|
||||
async def text_to_speech(self, text: Union[list[str], str], file_name: str):
|
||||
if isinstance(text, str):
|
||||
audio_segments = await self._text_to_speech(text)
|
||||
elif isinstance(text, list):
|
||||
audio_segments = await self._conversation_to_speech(text)
|
||||
else:
|
||||
raise ValueError("Unsupported argument for text_to_speech")
|
||||
|
||||
final_message = await self.synthesize_speech(
|
||||
"This audio recording, for the listening exercise, has finished.",
|
||||
"Stephen"
|
||||
)
|
||||
|
||||
# Add finish message
|
||||
audio_segments.append(final_message)
|
||||
|
||||
# Combine the audio segments into a single audio file
|
||||
combined_audio = b"".join(audio_segments)
|
||||
# Save the combined audio to a single file
|
||||
async with aiofiles.open(file_name, "wb") as f:
|
||||
await f.write(combined_audio)
|
||||
|
||||
print("Speech segments saved to " + file_name)
|
||||
|
||||
async def _text_to_speech(self, text: str):
|
||||
voice = random.choice(NeuralVoices.ALL_NEURAL_VOICES)['Id']
|
||||
# Initialize an empty list to store audio segments
|
||||
audio_segments = []
|
||||
for part in self._divide_text(text):
|
||||
audio_segments.append(await self.synthesize_speech(part, voice))
|
||||
|
||||
return audio_segments
|
||||
|
||||
async def _conversation_to_speech(self, conversation: list):
|
||||
# Initialize an empty list to store audio segments
|
||||
audio_segments = []
|
||||
# Iterate through the text segments, convert to audio segments, and store them
|
||||
for segment in conversation:
|
||||
audio_segments.append(await self.synthesize_speech(segment["text"], segment["voice"]))
|
||||
|
||||
return audio_segments
|
||||
|
||||
@staticmethod
|
||||
def _divide_text(text, max_length=3000):
|
||||
if len(text) <= max_length:
|
||||
return [text]
|
||||
|
||||
divisions = []
|
||||
current_position = 0
|
||||
|
||||
while current_position < len(text):
|
||||
next_position = min(current_position + max_length, len(text))
|
||||
next_period_position = text.rfind('.', current_position, next_position)
|
||||
|
||||
if next_period_position != -1 and next_period_position > current_position:
|
||||
divisions.append(text[current_position:next_period_position + 1])
|
||||
current_position = next_period_position + 1
|
||||
else:
|
||||
# If no '.' found in the next chunk, split at max_length
|
||||
divisions.append(text[current_position:next_position])
|
||||
current_position = next_position
|
||||
|
||||
return divisions
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
from logging import getLogger
|
||||
from typing import Dict, Optional
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.services.abc.third_parties.ai_detector import IAIDetectorService
|
||||
|
||||
|
||||
class GPTZero(IAIDetectorService):
|
||||
|
||||
_GPT_ZERO_ENDPOINT = 'https://api.gptzero.me/v2/predict/text'
|
||||
|
||||
def __init__(self, client: AsyncClient, gpt_zero_key: str):
|
||||
self._header = {
|
||||
'x-api-key': gpt_zero_key
|
||||
}
|
||||
self._http_client = client
|
||||
self._logger = getLogger(__name__)
|
||||
|
||||
async def run_detection(self, text: str):
|
||||
data = {
|
||||
'document': text,
|
||||
'version': '',
|
||||
'multilingual': False
|
||||
}
|
||||
|
||||
response = await self._http_client.post(self._GPT_ZERO_ENDPOINT, headers=self._header, json=data)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
return self._parse_detection(response.json())
|
||||
|
||||
def _parse_detection(self, response: Dict) -> Optional[Dict]:
|
||||
try:
|
||||
text_scan = response["documents"][0]
|
||||
|
||||
filtered_sentences = [
|
||||
{
|
||||
"sentence": item["sentence"],
|
||||
"highlight_sentence_for_ai": item["highlight_sentence_for_ai"]
|
||||
}
|
||||
for item in text_scan["sentences"]
|
||||
]
|
||||
|
||||
return {
|
||||
"class_probabilities": text_scan["class_probabilities"],
|
||||
"confidence_category": text_scan["confidence_category"],
|
||||
"predicted_class": text_scan["predicted_class"],
|
||||
"sentences": filtered_sentences
|
||||
}
|
||||
except Exception as e:
|
||||
self._logger.error(f'Failed to parse GPT\'s Zero response: {str(e)}')
|
||||
return None
|
||||
from logging import getLogger
|
||||
from typing import Dict, Optional
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.services.abc.third_parties.ai_detector import IAIDetectorService
|
||||
|
||||
|
||||
class GPTZero(IAIDetectorService):
|
||||
|
||||
_GPT_ZERO_ENDPOINT = 'https://api.gptzero.me/v2/predict/text'
|
||||
|
||||
def __init__(self, client: AsyncClient, gpt_zero_key: str):
|
||||
self._header = {
|
||||
'x-api-key': gpt_zero_key
|
||||
}
|
||||
self._http_client = client
|
||||
self._logger = getLogger(__name__)
|
||||
|
||||
async def run_detection(self, text: str):
|
||||
data = {
|
||||
'document': text,
|
||||
'version': '',
|
||||
'multilingual': False
|
||||
}
|
||||
|
||||
response = await self._http_client.post(self._GPT_ZERO_ENDPOINT, headers=self._header, json=data)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
return self._parse_detection(response.json())
|
||||
|
||||
def _parse_detection(self, response: Dict) -> Optional[Dict]:
|
||||
try:
|
||||
text_scan = response["documents"][0]
|
||||
|
||||
filtered_sentences = [
|
||||
{
|
||||
"sentence": item["sentence"],
|
||||
"highlight_sentence_for_ai": item["highlight_sentence_for_ai"]
|
||||
}
|
||||
for item in text_scan["sentences"]
|
||||
]
|
||||
|
||||
return {
|
||||
"class_probabilities": text_scan["class_probabilities"],
|
||||
"confidence_category": text_scan["confidence_category"],
|
||||
"predicted_class": text_scan["predicted_class"],
|
||||
"sentences": filtered_sentences
|
||||
}
|
||||
except Exception as e:
|
||||
self._logger.error(f'Failed to parse GPT\'s Zero response: {str(e)}')
|
||||
return None
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import aiofiles
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
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):
|
||||
self._get_header = {
|
||||
'X-Api-Key': heygen_token
|
||||
}
|
||||
self._post_header = {
|
||||
'X-Api-Key': heygen_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
self._http_client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def create_video(self, text: str, avatar: str):
|
||||
# POST TO CREATE VIDEO
|
||||
create_video_url = 'https://api.heygen.com/v2/template/' + avatar + '/generate'
|
||||
data = {
|
||||
"test": False,
|
||||
"caption": False,
|
||||
"title": "video_title",
|
||||
"variables": {
|
||||
"script_here": {
|
||||
"name": "script_here",
|
||||
"type": "text",
|
||||
"properties": {
|
||||
"content": text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response = await self._http_client.post(create_video_url, headers=self._post_header, json=data)
|
||||
self._logger.info(response.status_code)
|
||||
self._logger.info(response.json())
|
||||
|
||||
# GET TO CHECK STATUS AND GET VIDEO WHEN READY
|
||||
video_id = response.json()["data"]["video_id"]
|
||||
params = {
|
||||
'video_id': response.json()["data"]["video_id"]
|
||||
}
|
||||
response = {}
|
||||
status = "processing"
|
||||
error = None
|
||||
|
||||
while status != "completed" and error is None:
|
||||
response = await self._http_client.get(self._GET_VIDEO_URL, headers=self._get_header, params=params)
|
||||
response_data = response.json()
|
||||
|
||||
status = response_data["data"]["status"]
|
||||
error = response_data["data"]["error"]
|
||||
|
||||
if status != "completed" and error is None:
|
||||
self._logger.info(f"Status: {status}")
|
||||
await asyncio.sleep(10) # Wait for 10 second before the next request
|
||||
|
||||
self._logger.info(response.status_code)
|
||||
self._logger.info(response.json())
|
||||
|
||||
# DOWNLOAD VIDEO
|
||||
download_url = response.json()['data']['video_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) # Create the directory if it doesn't exist
|
||||
output_path = os.path.join(output_directory, output_filename)
|
||||
async with aiofiles.open(output_path, 'wb') as f:
|
||||
await 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
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import aiofiles
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
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):
|
||||
self._get_header = {
|
||||
'X-Api-Key': heygen_token
|
||||
}
|
||||
self._post_header = {
|
||||
'X-Api-Key': heygen_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
self._http_client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
async def create_video(self, text: str, avatar: str):
|
||||
# POST TO CREATE VIDEO
|
||||
create_video_url = 'https://api.heygen.com/v2/template/' + avatar + '/generate'
|
||||
data = {
|
||||
"test": False,
|
||||
"caption": False,
|
||||
"title": "video_title",
|
||||
"variables": {
|
||||
"script_here": {
|
||||
"name": "script_here",
|
||||
"type": "text",
|
||||
"properties": {
|
||||
"content": text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response = await self._http_client.post(create_video_url, headers=self._post_header, json=data)
|
||||
self._logger.info(response.status_code)
|
||||
self._logger.info(response.json())
|
||||
|
||||
# GET TO CHECK STATUS AND GET VIDEO WHEN READY
|
||||
video_id = response.json()["data"]["video_id"]
|
||||
params = {
|
||||
'video_id': response.json()["data"]["video_id"]
|
||||
}
|
||||
response = {}
|
||||
status = "processing"
|
||||
error = None
|
||||
|
||||
while status != "completed" and error is None:
|
||||
response = await self._http_client.get(self._GET_VIDEO_URL, headers=self._get_header, params=params)
|
||||
response_data = response.json()
|
||||
|
||||
status = response_data["data"]["status"]
|
||||
error = response_data["data"]["error"]
|
||||
|
||||
if status != "completed" and error is None:
|
||||
self._logger.info(f"Status: {status}")
|
||||
await asyncio.sleep(10) # Wait for 10 second before the next request
|
||||
|
||||
self._logger.info(response.status_code)
|
||||
self._logger.info(response.json())
|
||||
|
||||
# DOWNLOAD VIDEO
|
||||
download_url = response.json()['data']['video_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) # Create the directory if it doesn't exist
|
||||
output_path = os.path.join(output_directory, output_filename)
|
||||
async with aiofiles.open(output_path, 'wb') as f:
|
||||
await 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
|
||||
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Optional, Callable, TypeVar
|
||||
from openai import AsyncOpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
|
||||
from app.services.abc import ILLMService
|
||||
from app.helpers import count_tokens
|
||||
from app.configs.constants import BLACKLISTED_WORDS
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
|
||||
class OpenAI(ILLMService):
|
||||
|
||||
MAX_TOKENS = 4097
|
||||
TRY_LIMIT = 2
|
||||
|
||||
def __init__(self, client: AsyncOpenAI):
|
||||
self._client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._default_model = "gpt-4o-2024-08-06"
|
||||
|
||||
async def prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
check_blacklisted: bool = True,
|
||||
token_count: int = -1
|
||||
):
|
||||
if token_count == -1:
|
||||
token_count = self._count_total_tokens(messages)
|
||||
return await self._prediction(model, messages, token_count, fields_to_check, temperature, 0, check_blacklisted)
|
||||
|
||||
async def _prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
token_count: int,
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
try_count: int,
|
||||
check_blacklisted: bool,
|
||||
):
|
||||
result = await self._client.chat.completions.create(
|
||||
model=model,
|
||||
max_tokens=int(self.MAX_TOKENS - token_count - 300),
|
||||
temperature=float(temperature),
|
||||
messages=messages,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
result = result.choices[0].message.content
|
||||
|
||||
if check_blacklisted:
|
||||
found_blacklisted_word = self._get_found_blacklisted_words(result)
|
||||
|
||||
if found_blacklisted_word is not None and try_count < self.TRY_LIMIT:
|
||||
self._logger.warning("Result contains blacklisted words: " + str(found_blacklisted_word))
|
||||
return await self._prediction(
|
||||
model, messages, token_count, fields_to_check, temperature, (try_count + 1), check_blacklisted
|
||||
)
|
||||
elif found_blacklisted_word is not None and try_count >= self.TRY_LIMIT:
|
||||
return ""
|
||||
|
||||
if fields_to_check is None:
|
||||
return json.loads(result)
|
||||
|
||||
if not self._check_fields(result, fields_to_check) and try_count < self.TRY_LIMIT:
|
||||
return await self._prediction(
|
||||
model, messages, token_count, fields_to_check, temperature, (try_count + 1), check_blacklisted
|
||||
)
|
||||
|
||||
return json.loads(result)
|
||||
|
||||
async def prediction_override(self, **kwargs):
|
||||
return await self._client.chat.completions.create(
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_found_blacklisted_words(text: str):
|
||||
text_lower = text.lower()
|
||||
for word in BLACKLISTED_WORDS:
|
||||
if re.search(r'\b' + re.escape(word) + r'\b', text_lower):
|
||||
return word
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _count_total_tokens(messages):
|
||||
total_tokens = 0
|
||||
for message in messages:
|
||||
total_tokens += count_tokens(message["content"])["n_tokens"]
|
||||
return total_tokens
|
||||
|
||||
@staticmethod
|
||||
def _check_fields(obj, fields):
|
||||
return all(field in obj for field in fields)
|
||||
|
||||
async def pydantic_prediction(
|
||||
self,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
map_to_model: Callable,
|
||||
json_scheme: str,
|
||||
*,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_retries: int = 3
|
||||
) -> List[T] | T | None:
|
||||
params = {
|
||||
"messages": messages,
|
||||
"response_format": {"type": "json_object"},
|
||||
"model": model if model else self._default_model
|
||||
}
|
||||
|
||||
if temperature:
|
||||
params["temperature"] = temperature
|
||||
|
||||
attempt = 0
|
||||
while attempt < max_retries:
|
||||
result = await self._client.chat.completions.create(**params)
|
||||
result_content = result.choices[0].message.content
|
||||
try:
|
||||
result_json = json.loads(result_content)
|
||||
return map_to_model(result_json)
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
self._logger.info(f"GPT returned malformed response: {result_content}\n {str(e)}")
|
||||
params["messages"] = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Your previous response wasn't in the json format I've explicitly told you to output. "
|
||||
f"In your next response, you will fix it and return me just the json I've asked."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"Previous response: {result_content}\n"
|
||||
f"JSON format: {json_scheme}"
|
||||
)
|
||||
}
|
||||
]
|
||||
if attempt >= max_retries:
|
||||
self._logger.error(f"Max retries exceeded!")
|
||||
return None
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Optional, Callable, TypeVar
|
||||
from openai import AsyncOpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
|
||||
from app.services.abc import ILLMService
|
||||
from app.helpers import count_tokens
|
||||
from app.configs.constants import BLACKLISTED_WORDS
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
|
||||
class OpenAI(ILLMService):
|
||||
|
||||
MAX_TOKENS = 4097
|
||||
TRY_LIMIT = 2
|
||||
|
||||
def __init__(self, client: AsyncOpenAI):
|
||||
self._client = client
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._default_model = "gpt-4o-2024-08-06"
|
||||
|
||||
async def prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
check_blacklisted: bool = True,
|
||||
token_count: int = -1
|
||||
):
|
||||
if token_count == -1:
|
||||
token_count = self._count_total_tokens(messages)
|
||||
return await self._prediction(model, messages, token_count, fields_to_check, temperature, 0, check_blacklisted)
|
||||
|
||||
async def _prediction(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
token_count: int,
|
||||
fields_to_check: Optional[List[str]],
|
||||
temperature: float,
|
||||
try_count: int,
|
||||
check_blacklisted: bool,
|
||||
):
|
||||
result = await self._client.chat.completions.create(
|
||||
model=model,
|
||||
max_tokens=int(self.MAX_TOKENS - token_count - 300),
|
||||
temperature=float(temperature),
|
||||
messages=messages,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
result = result.choices[0].message.content
|
||||
|
||||
if check_blacklisted:
|
||||
found_blacklisted_word = self._get_found_blacklisted_words(result)
|
||||
|
||||
if found_blacklisted_word is not None and try_count < self.TRY_LIMIT:
|
||||
self._logger.warning("Result contains blacklisted words: " + str(found_blacklisted_word))
|
||||
return await self._prediction(
|
||||
model, messages, token_count, fields_to_check, temperature, (try_count + 1), check_blacklisted
|
||||
)
|
||||
elif found_blacklisted_word is not None and try_count >= self.TRY_LIMIT:
|
||||
return ""
|
||||
|
||||
if fields_to_check is None:
|
||||
return json.loads(result)
|
||||
|
||||
if not self._check_fields(result, fields_to_check) and try_count < self.TRY_LIMIT:
|
||||
return await self._prediction(
|
||||
model, messages, token_count, fields_to_check, temperature, (try_count + 1), check_blacklisted
|
||||
)
|
||||
|
||||
return json.loads(result)
|
||||
|
||||
async def prediction_override(self, **kwargs):
|
||||
return await self._client.chat.completions.create(
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_found_blacklisted_words(text: str):
|
||||
text_lower = text.lower()
|
||||
for word in BLACKLISTED_WORDS:
|
||||
if re.search(r'\b' + re.escape(word) + r'\b', text_lower):
|
||||
return word
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _count_total_tokens(messages):
|
||||
total_tokens = 0
|
||||
for message in messages:
|
||||
total_tokens += count_tokens(message["content"])["n_tokens"]
|
||||
return total_tokens
|
||||
|
||||
@staticmethod
|
||||
def _check_fields(obj, fields):
|
||||
return all(field in obj for field in fields)
|
||||
|
||||
async def pydantic_prediction(
|
||||
self,
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
map_to_model: Callable,
|
||||
json_scheme: str,
|
||||
*,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_retries: int = 3
|
||||
) -> List[T] | T | None:
|
||||
params = {
|
||||
"messages": messages,
|
||||
"response_format": {"type": "json_object"},
|
||||
"model": model if model else self._default_model
|
||||
}
|
||||
|
||||
if temperature:
|
||||
params["temperature"] = temperature
|
||||
|
||||
attempt = 0
|
||||
while attempt < max_retries:
|
||||
result = await self._client.chat.completions.create(**params)
|
||||
result_content = result.choices[0].message.content
|
||||
try:
|
||||
result_json = json.loads(result_content)
|
||||
return map_to_model(result_json)
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
self._logger.info(f"GPT returned malformed response: {result_content}\n {str(e)}")
|
||||
params["messages"] = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Your previous response wasn't in the json format I've explicitly told you to output. "
|
||||
f"In your next response, you will fix it and return me just the json I've asked."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"Previous response: {result_content}\n"
|
||||
f"JSON format: {json_scheme}"
|
||||
)
|
||||
}
|
||||
]
|
||||
if attempt >= max_retries:
|
||||
self._logger.error(f"Max retries exceeded!")
|
||||
return None
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import os
|
||||
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from whisper import Whisper
|
||||
from app.services.abc import ISpeechToTextService
|
||||
|
||||
|
||||
class OpenAIWhisper(ISpeechToTextService):
|
||||
|
||||
def __init__(self, model: Whisper):
|
||||
self._model = model
|
||||
|
||||
async def speech_to_text(self, file_path):
|
||||
if os.path.exists(file_path):
|
||||
result = await run_in_threadpool(
|
||||
self._model.transcribe, file_path, fp16=False, language='English', verbose=False
|
||||
)
|
||||
return result["text"]
|
||||
else:
|
||||
print("File not found:", file_path)
|
||||
raise Exception("File " + file_path + " not found.")
|
||||
import os
|
||||
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from whisper import Whisper
|
||||
from app.services.abc import ISpeechToTextService
|
||||
|
||||
|
||||
class OpenAIWhisper(ISpeechToTextService):
|
||||
|
||||
def __init__(self, model: Whisper):
|
||||
self._model = model
|
||||
|
||||
async def speech_to_text(self, file_path):
|
||||
if os.path.exists(file_path):
|
||||
result = await run_in_threadpool(
|
||||
self._model.transcribe, file_path, fp16=False, language='English', verbose=False
|
||||
)
|
||||
return result["text"]
|
||||
else:
|
||||
print("File not found:", file_path)
|
||||
raise Exception("File " + file_path + " not found.")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .training import TrainingService
|
||||
from .kb import TrainingContentKnowledgeBase
|
||||
|
||||
__all__ = [
|
||||
"TrainingService",
|
||||
"TrainingContentKnowledgeBase"
|
||||
]
|
||||
from .training import TrainingService
|
||||
from .kb import TrainingContentKnowledgeBase
|
||||
|
||||
__all__ = [
|
||||
"TrainingService",
|
||||
"TrainingContentKnowledgeBase"
|
||||
]
|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
import json
|
||||
import os
|
||||
from logging import getLogger
|
||||
from typing import Dict, List
|
||||
|
||||
import faiss
|
||||
import pickle
|
||||
|
||||
from app.services.abc import IKnowledgeBase
|
||||
|
||||
|
||||
class TrainingContentKnowledgeBase(IKnowledgeBase):
|
||||
|
||||
def __init__(self, embeddings, path: str = 'pathways_2_rw_with_ids.json'):
|
||||
self._embedding_model = embeddings
|
||||
self._tips = None # self._read_json(path)
|
||||
self._category_metadata = None
|
||||
self._indices = None
|
||||
self.load_indices_and_metadata()
|
||||
self._logger = getLogger(__name__)
|
||||
|
||||
@staticmethod
|
||||
def _read_json(path: str) -> Dict[str, any]:
|
||||
with open(path, 'r', encoding="utf-8") as json_file:
|
||||
return json.loads(json_file.read())
|
||||
|
||||
def print_category_count(self):
|
||||
category_tips = {}
|
||||
for unit in self._tips['units']:
|
||||
for page in unit['pages']:
|
||||
for tip in page['tips']:
|
||||
category = tip['category'].lower().replace(" ", "_")
|
||||
if category not in category_tips:
|
||||
category_tips[category] = 0
|
||||
else:
|
||||
category_tips[category] = category_tips[category] + 1
|
||||
print(category_tips)
|
||||
|
||||
def create_embeddings_and_save_them(self) -> None:
|
||||
category_embeddings = {}
|
||||
category_metadata = {}
|
||||
|
||||
for unit in self._tips['units']:
|
||||
for page in unit['pages']:
|
||||
for tip in page['tips']:
|
||||
category = tip['category'].lower().replace(" ", "_")
|
||||
if category not in category_embeddings:
|
||||
category_embeddings[category] = []
|
||||
category_metadata[category] = []
|
||||
|
||||
category_embeddings[category].append(tip['embedding'])
|
||||
category_metadata[category].append({"id": tip['id'], "text": tip['text']})
|
||||
|
||||
category_indices = {}
|
||||
for category, embeddings in category_embeddings.items():
|
||||
embeddings_array = self._embedding_model.encode(embeddings)
|
||||
index = faiss.IndexFlatL2(embeddings_array.shape[1])
|
||||
index.add(embeddings_array)
|
||||
category_indices[category] = index
|
||||
|
||||
faiss.write_index(index, f"./faiss/{category}_tips_index.faiss")
|
||||
|
||||
with open("./faiss/tips_metadata.pkl", "wb") as f:
|
||||
pickle.dump(category_metadata, f)
|
||||
|
||||
def load_indices_and_metadata(
|
||||
self,
|
||||
directory: str = './faiss',
|
||||
suffix: str = '_tips_index.faiss',
|
||||
metadata_path: str = './faiss/tips_metadata.pkl'
|
||||
):
|
||||
files = os.listdir(directory)
|
||||
self._indices = {}
|
||||
for file in files:
|
||||
if file.endswith(suffix):
|
||||
self._indices[file[:-len(suffix)]] = faiss.read_index(f'{directory}/{file}')
|
||||
self._logger.info(f'Loaded embeddings for {file[:-len(suffix)]} category.')
|
||||
|
||||
with open(metadata_path, 'rb') as f:
|
||||
self._category_metadata = pickle.load(f)
|
||||
self._logger.info("Loaded tips metadata")
|
||||
|
||||
def query_knowledge_base(self, query: str, category: str, top_k: int = 5) -> List[Dict[str, str]]:
|
||||
query_embedding = self._embedding_model.encode([query])
|
||||
index = self._indices[category]
|
||||
D, I = index.search(query_embedding, top_k)
|
||||
results = [self._category_metadata[category][i] for i in I[0]]
|
||||
return results
|
||||
import json
|
||||
import os
|
||||
from logging import getLogger
|
||||
from typing import Dict, List
|
||||
|
||||
import faiss
|
||||
import pickle
|
||||
|
||||
from app.services.abc import IKnowledgeBase
|
||||
|
||||
|
||||
class TrainingContentKnowledgeBase(IKnowledgeBase):
|
||||
|
||||
def __init__(self, embeddings, path: str = 'pathways_2_rw_with_ids.json'):
|
||||
self._embedding_model = embeddings
|
||||
self._tips = None # self._read_json(path)
|
||||
self._category_metadata = None
|
||||
self._indices = None
|
||||
self.load_indices_and_metadata()
|
||||
self._logger = getLogger(__name__)
|
||||
|
||||
@staticmethod
|
||||
def _read_json(path: str) -> Dict[str, any]:
|
||||
with open(path, 'r', encoding="utf-8") as json_file:
|
||||
return json.loads(json_file.read())
|
||||
|
||||
def print_category_count(self):
|
||||
category_tips = {}
|
||||
for unit in self._tips['units']:
|
||||
for page in unit['pages']:
|
||||
for tip in page['tips']:
|
||||
category = tip['category'].lower().replace(" ", "_")
|
||||
if category not in category_tips:
|
||||
category_tips[category] = 0
|
||||
else:
|
||||
category_tips[category] = category_tips[category] + 1
|
||||
print(category_tips)
|
||||
|
||||
def create_embeddings_and_save_them(self) -> None:
|
||||
category_embeddings = {}
|
||||
category_metadata = {}
|
||||
|
||||
for unit in self._tips['units']:
|
||||
for page in unit['pages']:
|
||||
for tip in page['tips']:
|
||||
category = tip['category'].lower().replace(" ", "_")
|
||||
if category not in category_embeddings:
|
||||
category_embeddings[category] = []
|
||||
category_metadata[category] = []
|
||||
|
||||
category_embeddings[category].append(tip['embedding'])
|
||||
category_metadata[category].append({"id": tip['id'], "text": tip['text']})
|
||||
|
||||
category_indices = {}
|
||||
for category, embeddings in category_embeddings.items():
|
||||
embeddings_array = self._embedding_model.encode(embeddings)
|
||||
index = faiss.IndexFlatL2(embeddings_array.shape[1])
|
||||
index.add(embeddings_array)
|
||||
category_indices[category] = index
|
||||
|
||||
faiss.write_index(index, f"./faiss/{category}_tips_index.faiss")
|
||||
|
||||
with open("./faiss/tips_metadata.pkl", "wb") as f:
|
||||
pickle.dump(category_metadata, f)
|
||||
|
||||
def load_indices_and_metadata(
|
||||
self,
|
||||
directory: str = './faiss',
|
||||
suffix: str = '_tips_index.faiss',
|
||||
metadata_path: str = './faiss/tips_metadata.pkl'
|
||||
):
|
||||
files = os.listdir(directory)
|
||||
self._indices = {}
|
||||
for file in files:
|
||||
if file.endswith(suffix):
|
||||
self._indices[file[:-len(suffix)]] = faiss.read_index(f'{directory}/{file}')
|
||||
self._logger.info(f'Loaded embeddings for {file[:-len(suffix)]} category.')
|
||||
|
||||
with open(metadata_path, 'rb') as f:
|
||||
self._category_metadata = pickle.load(f)
|
||||
self._logger.info("Loaded tips metadata")
|
||||
|
||||
def query_knowledge_base(self, query: str, category: str, top_k: int = 5) -> List[Dict[str, str]]:
|
||||
query_embedding = self._embedding_model.encode([query])
|
||||
index = self._indices[category]
|
||||
D, I = index.search(query_embedding, top_k)
|
||||
results = [self._category_metadata[category][i] for i in I[0]]
|
||||
return results
|
||||
|
||||
@@ -1,459 +1,459 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
from logging import getLogger
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from app.configs.constants import TemperatureSettings, GPTModels
|
||||
from app.helpers import count_tokens
|
||||
from app.repositories.abc import IDocumentStore
|
||||
from app.services.abc import ILLMService, ITrainingService, IKnowledgeBase
|
||||
from app.dtos.training import *
|
||||
|
||||
|
||||
class TrainingService(ITrainingService):
|
||||
TOOLS = [
|
||||
'critical_thinking',
|
||||
'language_for_writing',
|
||||
'reading_skills',
|
||||
'strategy',
|
||||
'words',
|
||||
'writing_skills'
|
||||
]
|
||||
# strategy word_link ct_focus reading_skill word_partners writing_skill language_for_writing
|
||||
|
||||
def __init__(self, llm: ILLMService, firestore: IDocumentStore, training_kb: IKnowledgeBase):
|
||||
self._llm = llm
|
||||
self._db = firestore
|
||||
self._kb = training_kb
|
||||
self._logger = getLogger(__name__)
|
||||
|
||||
async def fetch_tips(self, context: str, question: str, answer: str, correct_answer: str):
|
||||
messages = self._get_question_tips(question, answer, correct_answer, context)
|
||||
|
||||
token_count = reduce(lambda count, item: count + count_tokens(item)['n_tokens'],
|
||||
map(lambda x: x["content"], filter(lambda x: "content" in x, messages)), 0)
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
messages,
|
||||
None,
|
||||
TemperatureSettings.TIPS_TEMPERATURE,
|
||||
token_count=token_count
|
||||
)
|
||||
|
||||
if isinstance(response, str):
|
||||
response = re.sub(r"^[a-zA-Z0-9_]+\:\s*", "", response)
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _get_question_tips(question: str, answer: str, correct_answer: str, context: str = None):
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"You are a IELTS exam program that analyzes incorrect answers to questions and gives tips to "
|
||||
"help students understand why it was a wrong answer and gives helpful insight for the future. "
|
||||
"The tip should refer to the context and question."
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
if not (context is None or context == ""):
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": f"This is the context for the question: {context}",
|
||||
})
|
||||
|
||||
messages.extend([
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the question: {question}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the answer: {answer}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the correct answer: {correct_answer}",
|
||||
}
|
||||
])
|
||||
|
||||
return messages
|
||||
|
||||
async def get_training_content(self, training_content: Dict) -> Dict:
|
||||
user, stats = training_content["userID"], training_content["stats"]
|
||||
exam_data, exam_map = await self._sort_out_solutions(stats)
|
||||
training_content = await self._get_exam_details_and_tips(exam_data)
|
||||
tips = self._query_kb(training_content.queries)
|
||||
usefull_tips = await self._get_usefull_tips(exam_data, tips)
|
||||
exam_map = self._merge_exam_map_with_details(exam_map, training_content.details)
|
||||
|
||||
weak_areas = {"weak_areas": []}
|
||||
for area in training_content.weak_areas:
|
||||
weak_areas["weak_areas"].append(area.dict())
|
||||
|
||||
training_doc = {
|
||||
'created_at': int(datetime.now().timestamp() * 1000),
|
||||
**exam_map,
|
||||
**usefull_tips.dict(),
|
||||
**weak_areas,
|
||||
"user": user
|
||||
}
|
||||
doc_id = await self._db.save_to_db('training', training_doc)
|
||||
return {
|
||||
"id": doc_id
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _merge_exam_map_with_details(exam_map: Dict[str, any], details: List[DetailsDTO]):
|
||||
new_exam_map = {"exams": []}
|
||||
for detail in details:
|
||||
new_exam_map["exams"].append({
|
||||
"id": detail.exam_id,
|
||||
"date": detail.date,
|
||||
"performance_comment": detail.performance_comment,
|
||||
"detailed_summary": detail.detailed_summary,
|
||||
**exam_map[detail.exam_id]
|
||||
})
|
||||
return new_exam_map
|
||||
|
||||
def _query_kb(self, queries: List[QueryDTO]):
|
||||
map_categories = {
|
||||
"critical_thinking": "ct_focus",
|
||||
"language_for_writing": "language_for_writing",
|
||||
"reading_skills": "reading_skill",
|
||||
"strategy": "strategy",
|
||||
"writing_skills": "writing_skill"
|
||||
}
|
||||
|
||||
tips = {"tips": []}
|
||||
for query in queries:
|
||||
if query.category == "words":
|
||||
tips["tips"].extend(
|
||||
self._kb.query_knowledge_base(query.text, "word_link")
|
||||
)
|
||||
tips["tips"].extend(
|
||||
self._kb.query_knowledge_base(query.text, "word_partners")
|
||||
)
|
||||
else:
|
||||
if query.category in map_categories:
|
||||
tips["tips"].extend(
|
||||
self._kb.query_knowledge_base(query.text, map_categories[query.category])
|
||||
)
|
||||
else:
|
||||
self._logger.info(f"GTP tried to query knowledge base for {query.category} and it doesn't exist.")
|
||||
return tips
|
||||
|
||||
async def _get_exam_details_and_tips(self, exam_data: Dict[str, any]) -> TrainingContentDTO:
|
||||
json_schema = (
|
||||
'{ "details": [{"exam_id": "", "date": 0, "performance_comment": "", "detailed_summary": ""}],'
|
||||
' "weak_areas": [{"area": "", "comment": ""}], "queries": [{"text": "", "category": ""}] }'
|
||||
)
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"I'm going to provide you with exam data, you will take the exam data and fill this json "
|
||||
f'schema : {json_schema}. "performance_comment" is a short sentence that describes the '
|
||||
'students\'s performance and main mistakes in a single exam, "detailed_summary" is a detailed '
|
||||
'summary of the student\'s performance, "weak_areas" are identified areas'
|
||||
' across all exams which need to be improved upon, for example, area "Grammar and Syntax" comment "Issues'
|
||||
' with sentence structure and punctuation.", the "queries" field is where you will write queries '
|
||||
'for tips that will be displayed to the student, the category attribute is a collection of '
|
||||
'embeddings and the text will be the text used to query the knowledge base. The categories are '
|
||||
f'the following [{", ".join(self.TOOLS)}]. The exam data will be a json where the key of the field '
|
||||
'"exams" is the exam id, an exam can be composed of multiple modules or single modules. The student'
|
||||
' will see your response so refrain from using phrasing like "The student" did x, y and z. If the '
|
||||
'field "answer" in a question is an empty array "[]", then the student didn\'t answer any question '
|
||||
'and you must address that in your response. Also questions aren\'t modules, the only modules are: '
|
||||
'level, speaking, writing, reading and listening. The details array needs to be tailored to the '
|
||||
'exam attempt, even if you receive the same exam you must treat as different exams by their id.'
|
||||
'Don\'t make references to an exam by it\'s id, the GUI will handle that so the student knows '
|
||||
'which is the exam your comments and summary are referencing too. Even if the student hasn\'t '
|
||||
'submitted no answers for an exam, you must still fill the details structure addressing that fact.'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Exam Data: {str(exam_data)}'
|
||||
}
|
||||
]
|
||||
return await self._llm.pydantic_prediction(messages, self._map_gpt_response, json_schema)
|
||||
|
||||
async def _get_usefull_tips(self, exam_data: Dict[str, any], tips: Dict[str, any]) -> TipsDTO:
|
||||
json_schema = (
|
||||
'{ "tip_ids": [] }'
|
||||
)
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"I'm going to provide you with tips and I want you to return to me the tips that "
|
||||
f"can be usefull for the student that made the exam that I'm going to send you, return "
|
||||
f"me the tip ids in this json format {json_schema}."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Exam Data: {str(exam_data)}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Tips: {str(tips)}'
|
||||
}
|
||||
]
|
||||
return await self._llm.pydantic_prediction(messages, lambda response: TipsDTO(**response), json_schema)
|
||||
|
||||
@staticmethod
|
||||
def _map_gpt_response(response: Dict[str, any]) -> TrainingContentDTO:
|
||||
parsed_response = {
|
||||
"details": [DetailsDTO(**detail) for detail in response["details"]],
|
||||
"weak_areas": [WeakAreaDTO(**area) for area in response["weak_areas"]],
|
||||
"queries": [QueryDTO(**query) for query in response["queries"]]
|
||||
}
|
||||
return TrainingContentDTO(**parsed_response)
|
||||
|
||||
async def _sort_out_solutions(self, stats):
|
||||
grouped_stats = {}
|
||||
for stat in stats:
|
||||
session_key = f'{str(stat["date"])}-{stat["user"]}'
|
||||
module = stat["module"]
|
||||
exam_id = stat["exam"]
|
||||
|
||||
if session_key not in grouped_stats:
|
||||
grouped_stats[session_key] = {}
|
||||
if module not in grouped_stats[session_key]:
|
||||
grouped_stats[session_key][module] = {
|
||||
"stats": [],
|
||||
"exam_id": exam_id
|
||||
}
|
||||
grouped_stats[session_key][module]["stats"].append(stat)
|
||||
|
||||
exercises = {}
|
||||
exam_map = {}
|
||||
for session_key, modules in grouped_stats.items():
|
||||
exercises[session_key] = {}
|
||||
for module, module_stats in modules.items():
|
||||
exercises[session_key][module] = {}
|
||||
|
||||
exam_id = module_stats["exam_id"]
|
||||
if exam_id not in exercises[session_key][module]:
|
||||
exercises[session_key][module][exam_id] = {"date": None, "exercises": []}
|
||||
|
||||
exam_total_questions = 0
|
||||
exam_total_correct = 0
|
||||
|
||||
for stat in module_stats["stats"]:
|
||||
exam_total_questions += stat["score"]["total"]
|
||||
exam_total_correct += stat["score"]["correct"]
|
||||
exercises[session_key][module][exam_id]["date"] = stat["date"]
|
||||
|
||||
if session_key not in exam_map:
|
||||
exam_map[session_key] = {"stat_ids": [], "score": 0}
|
||||
exam_map[session_key]["stat_ids"].append(stat["id"])
|
||||
|
||||
exam = await self._db.get_doc_by_id(module, exam_id)
|
||||
if module == "listening":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_listening_solutions(stat, exam))
|
||||
elif module == "reading":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_reading_solutions(stat, exam))
|
||||
elif module == "writing":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_writing_prompts_and_answers(stat, exam)
|
||||
)
|
||||
elif module == "speaking":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_speaking_solutions(stat, exam)
|
||||
)
|
||||
elif module == "level":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_level_solutions(stat, exam)
|
||||
)
|
||||
|
||||
exam_map[session_key]["score"] = round((exam_total_correct / exam_total_questions) * 100)
|
||||
exam_map[session_key]["module"] = module
|
||||
|
||||
return {"exams": exercises}, exam_map
|
||||
|
||||
def _get_writing_prompts_and_answers(self, stat, exam):
|
||||
result = []
|
||||
try:
|
||||
exercises = []
|
||||
for solution in stat['solutions']:
|
||||
answer = solution['solution']
|
||||
exercise_id = solution['id']
|
||||
exercises.append({
|
||||
"exercise_id": exercise_id,
|
||||
"answer": answer
|
||||
})
|
||||
for exercise in exercises:
|
||||
for exam_exercise in exam["exercises"]:
|
||||
if exam_exercise["id"] == exercise["exercise_id"]:
|
||||
result.append({
|
||||
"exercise": exam_exercise["prompt"],
|
||||
"answer": exercise["answer"]
|
||||
})
|
||||
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_mc_question(exercise, stat):
|
||||
shuffle_maps = stat.get("shuffleMaps", [])
|
||||
answer = stat["solutions"] if len(shuffle_maps) == 0 else []
|
||||
if len(shuffle_maps) != 0:
|
||||
for solution in stat["solutions"]:
|
||||
shuffle_map = [
|
||||
item["map"] for item in shuffle_maps
|
||||
if item["questionID"] == solution["question"]
|
||||
]
|
||||
answer.append({
|
||||
"question": solution["question"],
|
||||
"option": shuffle_map[solution["option"]]
|
||||
})
|
||||
return {
|
||||
"question": exercise["prompt"],
|
||||
"exercise": exercise["questions"],
|
||||
"answer": stat["solutions"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _swap_key_name(d, original_key, new_key):
|
||||
d[new_key] = d.pop(original_key)
|
||||
return d
|
||||
|
||||
def _get_level_solutions(self, stat, exam):
|
||||
result = []
|
||||
try:
|
||||
for part in exam["parts"]:
|
||||
for exercise in part["exercises"]:
|
||||
if exercise["id"] == stat["exercise"]:
|
||||
if stat["type"] == "fillBlanks":
|
||||
result.append({
|
||||
"prompt": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"words": exercise["words"],
|
||||
"solutions": exercise["solutions"],
|
||||
"answer": [
|
||||
self._swap_key_name(item, 'solution', 'option')
|
||||
for item in stat["solutions"]
|
||||
]
|
||||
})
|
||||
elif stat["type"] == "multipleChoice":
|
||||
result.append(self._get_mc_question(exercise, stat))
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
return result
|
||||
|
||||
def _get_listening_solutions(self, stat, exam):
|
||||
result = []
|
||||
try:
|
||||
for part in exam["parts"]:
|
||||
for exercise in part["exercises"]:
|
||||
if exercise["id"] == stat["exercise"]:
|
||||
if stat["type"] == "writeBlanks":
|
||||
result.append({
|
||||
"question": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"solution": exercise["solutions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "fillBlanks":
|
||||
result.append({
|
||||
"question": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"words": exercise["words"],
|
||||
"solutions": exercise["solutions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "multipleChoice":
|
||||
result.append(self._get_mc_question(exercise, stat))
|
||||
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _find_shuffle_map(shuffle_maps, question_id):
|
||||
return next((item["map"] for item in shuffle_maps if item["questionID"] == question_id), None)
|
||||
|
||||
def _get_speaking_solutions(self, stat, exam):
|
||||
result = {}
|
||||
try:
|
||||
result = {
|
||||
"comments": {
|
||||
key: value['comment'] for key, value in stat['solutions'][0]['evaluation']['task_response'].items()}
|
||||
,
|
||||
"exercises": {}
|
||||
}
|
||||
|
||||
for exercise in exam["exercises"]:
|
||||
if exercise["id"] == stat["exercise"]:
|
||||
if stat["type"] == "interactiveSpeaking":
|
||||
for i in range(len(exercise["prompts"])):
|
||||
result["exercises"][f"exercise_{i+1}"] = {
|
||||
"question": exercise["prompts"][i]["text"]
|
||||
}
|
||||
for i in range(len(exercise["prompts"])):
|
||||
answer = stat['solutions'][0]["evaluation"].get(f'transcript_{i+1}', '')
|
||||
result["exercises"][f"exercise_{i+1}"]["answer"] = answer
|
||||
elif stat["type"] == "speaking":
|
||||
result["exercises"]["exercise_1"] = {
|
||||
"question": exercise["text"],
|
||||
"answer": stat['solutions'][0]["evaluation"].get(f'transcript', '')
|
||||
}
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
return [result]
|
||||
|
||||
def _get_reading_solutions(self, stat, exam):
|
||||
result = []
|
||||
try:
|
||||
for part in exam["parts"]:
|
||||
text = part["text"]
|
||||
for exercise in part["exercises"]:
|
||||
if exercise["id"] == stat["exercise"]:
|
||||
if stat["type"] == "fillBlanks":
|
||||
result.append({
|
||||
"text": text,
|
||||
"question": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"words": exercise["words"],
|
||||
"solutions": exercise["solutions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "writeBlanks":
|
||||
result.append({
|
||||
"text": text,
|
||||
"question": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"solutions": exercise["solutions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "trueFalse":
|
||||
result.append({
|
||||
"text": text,
|
||||
"questions": exercise["questions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "matchSentences":
|
||||
result.append({
|
||||
"text": text,
|
||||
"question": exercise["prompt"],
|
||||
"sentences": exercise["sentences"],
|
||||
"options": exercise["options"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
return result
|
||||
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
from logging import getLogger
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from app.configs.constants import TemperatureSettings, GPTModels
|
||||
from app.helpers import count_tokens
|
||||
from app.repositories.abc import IDocumentStore
|
||||
from app.services.abc import ILLMService, ITrainingService, IKnowledgeBase
|
||||
from app.dtos.training import *
|
||||
|
||||
|
||||
class TrainingService(ITrainingService):
|
||||
TOOLS = [
|
||||
'critical_thinking',
|
||||
'language_for_writing',
|
||||
'reading_skills',
|
||||
'strategy',
|
||||
'words',
|
||||
'writing_skills'
|
||||
]
|
||||
# strategy word_link ct_focus reading_skill word_partners writing_skill language_for_writing
|
||||
|
||||
def __init__(self, llm: ILLMService, firestore: IDocumentStore, training_kb: IKnowledgeBase):
|
||||
self._llm = llm
|
||||
self._db = firestore
|
||||
self._kb = training_kb
|
||||
self._logger = getLogger(__name__)
|
||||
|
||||
async def fetch_tips(self, context: str, question: str, answer: str, correct_answer: str):
|
||||
messages = self._get_question_tips(question, answer, correct_answer, context)
|
||||
|
||||
token_count = reduce(lambda count, item: count + count_tokens(item)['n_tokens'],
|
||||
map(lambda x: x["content"], filter(lambda x: "content" in x, messages)), 0)
|
||||
|
||||
response = await self._llm.prediction(
|
||||
GPTModels.GPT_3_5_TURBO,
|
||||
messages,
|
||||
None,
|
||||
TemperatureSettings.TIPS_TEMPERATURE,
|
||||
token_count=token_count
|
||||
)
|
||||
|
||||
if isinstance(response, str):
|
||||
response = re.sub(r"^[a-zA-Z0-9_]+\:\s*", "", response)
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _get_question_tips(question: str, answer: str, correct_answer: str, context: str = None):
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"You are a IELTS exam program that analyzes incorrect answers to questions and gives tips to "
|
||||
"help students understand why it was a wrong answer and gives helpful insight for the future. "
|
||||
"The tip should refer to the context and question."
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
if not (context is None or context == ""):
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": f"This is the context for the question: {context}",
|
||||
})
|
||||
|
||||
messages.extend([
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the question: {question}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the answer: {answer}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"This is the correct answer: {correct_answer}",
|
||||
}
|
||||
])
|
||||
|
||||
return messages
|
||||
|
||||
async def get_training_content(self, training_content: Dict) -> Dict:
|
||||
user, stats = training_content["userID"], training_content["stats"]
|
||||
exam_data, exam_map = await self._sort_out_solutions(stats)
|
||||
training_content = await self._get_exam_details_and_tips(exam_data)
|
||||
tips = self._query_kb(training_content.queries)
|
||||
usefull_tips = await self._get_usefull_tips(exam_data, tips)
|
||||
exam_map = self._merge_exam_map_with_details(exam_map, training_content.details)
|
||||
|
||||
weak_areas = {"weak_areas": []}
|
||||
for area in training_content.weak_areas:
|
||||
weak_areas["weak_areas"].append(area.dict())
|
||||
|
||||
training_doc = {
|
||||
'created_at': int(datetime.now().timestamp() * 1000),
|
||||
**exam_map,
|
||||
**usefull_tips.dict(),
|
||||
**weak_areas,
|
||||
"user": user
|
||||
}
|
||||
doc_id = await self._db.save_to_db('training', training_doc)
|
||||
return {
|
||||
"id": doc_id
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _merge_exam_map_with_details(exam_map: Dict[str, any], details: List[DetailsDTO]):
|
||||
new_exam_map = {"exams": []}
|
||||
for detail in details:
|
||||
new_exam_map["exams"].append({
|
||||
"id": detail.exam_id,
|
||||
"date": detail.date,
|
||||
"performance_comment": detail.performance_comment,
|
||||
"detailed_summary": detail.detailed_summary,
|
||||
**exam_map[detail.exam_id]
|
||||
})
|
||||
return new_exam_map
|
||||
|
||||
def _query_kb(self, queries: List[QueryDTO]):
|
||||
map_categories = {
|
||||
"critical_thinking": "ct_focus",
|
||||
"language_for_writing": "language_for_writing",
|
||||
"reading_skills": "reading_skill",
|
||||
"strategy": "strategy",
|
||||
"writing_skills": "writing_skill"
|
||||
}
|
||||
|
||||
tips = {"tips": []}
|
||||
for query in queries:
|
||||
if query.category == "words":
|
||||
tips["tips"].extend(
|
||||
self._kb.query_knowledge_base(query.text, "word_link")
|
||||
)
|
||||
tips["tips"].extend(
|
||||
self._kb.query_knowledge_base(query.text, "word_partners")
|
||||
)
|
||||
else:
|
||||
if query.category in map_categories:
|
||||
tips["tips"].extend(
|
||||
self._kb.query_knowledge_base(query.text, map_categories[query.category])
|
||||
)
|
||||
else:
|
||||
self._logger.info(f"GTP tried to query knowledge base for {query.category} and it doesn't exist.")
|
||||
return tips
|
||||
|
||||
async def _get_exam_details_and_tips(self, exam_data: Dict[str, any]) -> TrainingContentDTO:
|
||||
json_schema = (
|
||||
'{ "details": [{"exam_id": "", "date": 0, "performance_comment": "", "detailed_summary": ""}],'
|
||||
' "weak_areas": [{"area": "", "comment": ""}], "queries": [{"text": "", "category": ""}] }'
|
||||
)
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"I'm going to provide you with exam data, you will take the exam data and fill this json "
|
||||
f'schema : {json_schema}. "performance_comment" is a short sentence that describes the '
|
||||
'students\'s performance and main mistakes in a single exam, "detailed_summary" is a detailed '
|
||||
'summary of the student\'s performance, "weak_areas" are identified areas'
|
||||
' across all exams which need to be improved upon, for example, area "Grammar and Syntax" comment "Issues'
|
||||
' with sentence structure and punctuation.", the "queries" field is where you will write queries '
|
||||
'for tips that will be displayed to the student, the category attribute is a collection of '
|
||||
'embeddings and the text will be the text used to query the knowledge base. The categories are '
|
||||
f'the following [{", ".join(self.TOOLS)}]. The exam data will be a json where the key of the field '
|
||||
'"exams" is the exam id, an exam can be composed of multiple modules or single modules. The student'
|
||||
' will see your response so refrain from using phrasing like "The student" did x, y and z. If the '
|
||||
'field "answer" in a question is an empty array "[]", then the student didn\'t answer any question '
|
||||
'and you must address that in your response. Also questions aren\'t modules, the only modules are: '
|
||||
'level, speaking, writing, reading and listening. The details array needs to be tailored to the '
|
||||
'exam attempt, even if you receive the same exam you must treat as different exams by their id.'
|
||||
'Don\'t make references to an exam by it\'s id, the GUI will handle that so the student knows '
|
||||
'which is the exam your comments and summary are referencing too. Even if the student hasn\'t '
|
||||
'submitted no answers for an exam, you must still fill the details structure addressing that fact.'
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Exam Data: {str(exam_data)}'
|
||||
}
|
||||
]
|
||||
return await self._llm.pydantic_prediction(messages, self._map_gpt_response, json_schema)
|
||||
|
||||
async def _get_usefull_tips(self, exam_data: Dict[str, any], tips: Dict[str, any]) -> TipsDTO:
|
||||
json_schema = (
|
||||
'{ "tip_ids": [] }'
|
||||
)
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"I'm going to provide you with tips and I want you to return to me the tips that "
|
||||
f"can be usefull for the student that made the exam that I'm going to send you, return "
|
||||
f"me the tip ids in this json format {json_schema}."
|
||||
)
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Exam Data: {str(exam_data)}'
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f'Tips: {str(tips)}'
|
||||
}
|
||||
]
|
||||
return await self._llm.pydantic_prediction(messages, lambda response: TipsDTO(**response), json_schema)
|
||||
|
||||
@staticmethod
|
||||
def _map_gpt_response(response: Dict[str, any]) -> TrainingContentDTO:
|
||||
parsed_response = {
|
||||
"details": [DetailsDTO(**detail) for detail in response["details"]],
|
||||
"weak_areas": [WeakAreaDTO(**area) for area in response["weak_areas"]],
|
||||
"queries": [QueryDTO(**query) for query in response["queries"]]
|
||||
}
|
||||
return TrainingContentDTO(**parsed_response)
|
||||
|
||||
async def _sort_out_solutions(self, stats):
|
||||
grouped_stats = {}
|
||||
for stat in stats:
|
||||
session_key = f'{str(stat["date"])}-{stat["user"]}'
|
||||
module = stat["module"]
|
||||
exam_id = stat["exam"]
|
||||
|
||||
if session_key not in grouped_stats:
|
||||
grouped_stats[session_key] = {}
|
||||
if module not in grouped_stats[session_key]:
|
||||
grouped_stats[session_key][module] = {
|
||||
"stats": [],
|
||||
"exam_id": exam_id
|
||||
}
|
||||
grouped_stats[session_key][module]["stats"].append(stat)
|
||||
|
||||
exercises = {}
|
||||
exam_map = {}
|
||||
for session_key, modules in grouped_stats.items():
|
||||
exercises[session_key] = {}
|
||||
for module, module_stats in modules.items():
|
||||
exercises[session_key][module] = {}
|
||||
|
||||
exam_id = module_stats["exam_id"]
|
||||
if exam_id not in exercises[session_key][module]:
|
||||
exercises[session_key][module][exam_id] = {"date": None, "exercises": []}
|
||||
|
||||
exam_total_questions = 0
|
||||
exam_total_correct = 0
|
||||
|
||||
for stat in module_stats["stats"]:
|
||||
exam_total_questions += stat["score"]["total"]
|
||||
exam_total_correct += stat["score"]["correct"]
|
||||
exercises[session_key][module][exam_id]["date"] = stat["date"]
|
||||
|
||||
if session_key not in exam_map:
|
||||
exam_map[session_key] = {"stat_ids": [], "score": 0}
|
||||
exam_map[session_key]["stat_ids"].append(stat["id"])
|
||||
|
||||
exam = await self._db.get_doc_by_id(module, exam_id)
|
||||
if module == "listening":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_listening_solutions(stat, exam))
|
||||
elif module == "reading":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_reading_solutions(stat, exam))
|
||||
elif module == "writing":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_writing_prompts_and_answers(stat, exam)
|
||||
)
|
||||
elif module == "speaking":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_speaking_solutions(stat, exam)
|
||||
)
|
||||
elif module == "level":
|
||||
exercises[session_key][module][exam_id]["exercises"].extend(
|
||||
self._get_level_solutions(stat, exam)
|
||||
)
|
||||
|
||||
exam_map[session_key]["score"] = round((exam_total_correct / exam_total_questions) * 100)
|
||||
exam_map[session_key]["module"] = module
|
||||
|
||||
return {"exams": exercises}, exam_map
|
||||
|
||||
def _get_writing_prompts_and_answers(self, stat, exam):
|
||||
result = []
|
||||
try:
|
||||
exercises = []
|
||||
for solution in stat['solutions']:
|
||||
answer = solution['solution']
|
||||
exercise_id = solution['id']
|
||||
exercises.append({
|
||||
"exercise_id": exercise_id,
|
||||
"answer": answer
|
||||
})
|
||||
for exercise in exercises:
|
||||
for exam_exercise in exam["exercises"]:
|
||||
if exam_exercise["id"] == exercise["exercise_id"]:
|
||||
result.append({
|
||||
"exercise": exam_exercise["prompt"],
|
||||
"answer": exercise["answer"]
|
||||
})
|
||||
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_mc_question(exercise, stat):
|
||||
shuffle_maps = stat.get("shuffleMaps", [])
|
||||
answer = stat["solutions"] if len(shuffle_maps) == 0 else []
|
||||
if len(shuffle_maps) != 0:
|
||||
for solution in stat["solutions"]:
|
||||
shuffle_map = [
|
||||
item["map"] for item in shuffle_maps
|
||||
if item["questionID"] == solution["question"]
|
||||
]
|
||||
answer.append({
|
||||
"question": solution["question"],
|
||||
"option": shuffle_map[solution["option"]]
|
||||
})
|
||||
return {
|
||||
"question": exercise["prompt"],
|
||||
"exercise": exercise["questions"],
|
||||
"answer": stat["solutions"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _swap_key_name(d, original_key, new_key):
|
||||
d[new_key] = d.pop(original_key)
|
||||
return d
|
||||
|
||||
def _get_level_solutions(self, stat, exam):
|
||||
result = []
|
||||
try:
|
||||
for part in exam["parts"]:
|
||||
for exercise in part["exercises"]:
|
||||
if exercise["id"] == stat["exercise"]:
|
||||
if stat["type"] == "fillBlanks":
|
||||
result.append({
|
||||
"prompt": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"words": exercise["words"],
|
||||
"solutions": exercise["solutions"],
|
||||
"answer": [
|
||||
self._swap_key_name(item, 'solution', 'option')
|
||||
for item in stat["solutions"]
|
||||
]
|
||||
})
|
||||
elif stat["type"] == "multipleChoice":
|
||||
result.append(self._get_mc_question(exercise, stat))
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
return result
|
||||
|
||||
def _get_listening_solutions(self, stat, exam):
|
||||
result = []
|
||||
try:
|
||||
for part in exam["parts"]:
|
||||
for exercise in part["exercises"]:
|
||||
if exercise["id"] == stat["exercise"]:
|
||||
if stat["type"] == "writeBlanks":
|
||||
result.append({
|
||||
"question": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"solution": exercise["solutions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "fillBlanks":
|
||||
result.append({
|
||||
"question": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"words": exercise["words"],
|
||||
"solutions": exercise["solutions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "multipleChoice":
|
||||
result.append(self._get_mc_question(exercise, stat))
|
||||
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _find_shuffle_map(shuffle_maps, question_id):
|
||||
return next((item["map"] for item in shuffle_maps if item["questionID"] == question_id), None)
|
||||
|
||||
def _get_speaking_solutions(self, stat, exam):
|
||||
result = {}
|
||||
try:
|
||||
result = {
|
||||
"comments": {
|
||||
key: value['comment'] for key, value in stat['solutions'][0]['evaluation']['task_response'].items()}
|
||||
,
|
||||
"exercises": {}
|
||||
}
|
||||
|
||||
for exercise in exam["exercises"]:
|
||||
if exercise["id"] == stat["exercise"]:
|
||||
if stat["type"] == "interactiveSpeaking":
|
||||
for i in range(len(exercise["prompts"])):
|
||||
result["exercises"][f"exercise_{i+1}"] = {
|
||||
"question": exercise["prompts"][i]["text"]
|
||||
}
|
||||
for i in range(len(exercise["prompts"])):
|
||||
answer = stat['solutions'][0]["evaluation"].get(f'transcript_{i+1}', '')
|
||||
result["exercises"][f"exercise_{i+1}"]["answer"] = answer
|
||||
elif stat["type"] == "speaking":
|
||||
result["exercises"]["exercise_1"] = {
|
||||
"question": exercise["text"],
|
||||
"answer": stat['solutions'][0]["evaluation"].get(f'transcript', '')
|
||||
}
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
return [result]
|
||||
|
||||
def _get_reading_solutions(self, stat, exam):
|
||||
result = []
|
||||
try:
|
||||
for part in exam["parts"]:
|
||||
text = part["text"]
|
||||
for exercise in part["exercises"]:
|
||||
if exercise["id"] == stat["exercise"]:
|
||||
if stat["type"] == "fillBlanks":
|
||||
result.append({
|
||||
"text": text,
|
||||
"question": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"words": exercise["words"],
|
||||
"solutions": exercise["solutions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "writeBlanks":
|
||||
result.append({
|
||||
"text": text,
|
||||
"question": exercise["prompt"],
|
||||
"template": exercise["text"],
|
||||
"solutions": exercise["solutions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "trueFalse":
|
||||
result.append({
|
||||
"text": text,
|
||||
"questions": exercise["questions"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
elif stat["type"] == "matchSentences":
|
||||
result.append({
|
||||
"text": text,
|
||||
"question": exercise["prompt"],
|
||||
"sentences": exercise["sentences"],
|
||||
"options": exercise["options"],
|
||||
"answer": stat["solutions"]
|
||||
})
|
||||
except KeyError as e:
|
||||
self._logger.warning(f"Malformed stat object: {str(e)}")
|
||||
return result
|
||||
|
||||
|
||||
|
||||
262
app/services/impl/user.py
Normal file
262
app/services/impl/user.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
import pandas as pd
|
||||
import shortuuid
|
||||
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
from pymongo.database import Database
|
||||
|
||||
from app.dtos.user_batch import BatchUsersDTO, UserDTO
|
||||
from app.helpers import FileHelper
|
||||
from app.services.abc import IUserService
|
||||
|
||||
|
||||
class UserService(IUserService):
|
||||
_DEFAULT_DESIRED_LEVELS = {
|
||||
"reading": 9,
|
||||
"listening": 9,
|
||||
"writing": 9,
|
||||
"speaking": 9,
|
||||
}
|
||||
|
||||
_DEFAULT_LEVELS = {
|
||||
"reading": 0,
|
||||
"listening": 0,
|
||||
"writing": 0,
|
||||
"speaking": 0,
|
||||
}
|
||||
|
||||
def __init__(self, mongo: Database):
|
||||
self._db: Database = mongo
|
||||
self._logger = getLogger(__name__)
|
||||
|
||||
def fetch_tips(self, batch: BatchUsersDTO):
|
||||
file_name = f'{uuid.uuid4()}.csv'
|
||||
path = f'./tmp/{file_name}'
|
||||
self._generate_firebase_auth_csv(batch, path)
|
||||
|
||||
result = self._upload_users('./tmp', file_name)
|
||||
if result.returncode != 0:
|
||||
error_msg = f"Couldn't upload users. Failed to run command firebase auth import -> ```cmd {result.stdout}```"
|
||||
self._logger.error(error_msg)
|
||||
return error_msg
|
||||
|
||||
self._init_users(batch)
|
||||
|
||||
FileHelper.remove_file(path)
|
||||
return {"ok": True}
|
||||
|
||||
@staticmethod
|
||||
def _generate_firebase_auth_csv(batch_dto: BatchUsersDTO, path: str):
|
||||
# https://firebase.google.com/docs/cli/auth#file_format
|
||||
columns = [
|
||||
'UID', 'Email', 'Email Verified', 'Password Hash', 'Password Salt', 'Name',
|
||||
'Photo URL', 'Google ID', 'Google Email', 'Google Display Name', 'Google Photo URL',
|
||||
'Facebook ID', 'Facebook Email', 'Facebook Display Name', 'Facebook Photo URL',
|
||||
'Twitter ID', 'Twitter Email', 'Twitter Display Name', 'Twitter Photo URL',
|
||||
'GitHub ID', 'GitHub Email', 'GitHub Display Name', 'GitHub Photo URL',
|
||||
'User Creation Time', 'Last Sign-In Time', 'Phone Number'
|
||||
]
|
||||
users_data = []
|
||||
|
||||
current_time = int(time.time() * 1000)
|
||||
|
||||
for user in batch_dto.users:
|
||||
user_data = {
|
||||
'UID': str(user.id),
|
||||
'Email': user.email,
|
||||
'Email Verified': False,
|
||||
'Password Hash': user.passwordHash,
|
||||
'Password Salt': user.passwordSalt,
|
||||
'Name': '',
|
||||
'Photo URL': '',
|
||||
'Google ID': '',
|
||||
'Google Email': '',
|
||||
'Google Display Name': '',
|
||||
'Google Photo URL': '',
|
||||
'Facebook ID': '',
|
||||
'Facebook Email': '',
|
||||
'Facebook Display Name': '',
|
||||
'Facebook Photo URL': '',
|
||||
'Twitter ID': '',
|
||||
'Twitter Email': '',
|
||||
'Twitter Display Name': '',
|
||||
'Twitter Photo URL': '',
|
||||
'GitHub ID': '',
|
||||
'GitHub Email': '',
|
||||
'GitHub Display Name': '',
|
||||
'GitHub Photo URL': '',
|
||||
'User Creation Time': current_time,
|
||||
'Last Sign-In Time': '',
|
||||
'Phone Number': ''
|
||||
}
|
||||
users_data.append(user_data)
|
||||
|
||||
df = pd.DataFrame(users_data, columns=columns)
|
||||
df.to_csv(path, index=False, header=False)
|
||||
|
||||
@staticmethod
|
||||
def _upload_users(directory: str, file_name: str):
|
||||
command = (
|
||||
f'firebase auth:import {file_name} '
|
||||
f'--hash-algo=SCRYPT '
|
||||
f'--hash-key={os.getenv("FIREBASE_SCRYPT_B64_SIGNER_KEY")} '
|
||||
f'--salt-separator={os.getenv("FIREBASE_SCRYPT_B64_SALT_SEPARATOR")} '
|
||||
f'--rounds={os.getenv("FIREBASE_SCRYPT_ROUNDS")} '
|
||||
f'--mem-cost={os.getenv("FIREBASE_SCRYPT_MEM_COST")} '
|
||||
f'--project={os.getenv("FIREBASE_PROJECT_ID")} '
|
||||
)
|
||||
|
||||
result = subprocess.run(command, shell=True, cwd=directory, capture_output=True, text=True)
|
||||
return result
|
||||
|
||||
def _init_users(self, batch_users: BatchUsersDTO):
|
||||
maker_id = batch_users.makerID
|
||||
for user in batch_users.users:
|
||||
self._insert_new_user(user)
|
||||
code = self._create_code(user, maker_id)
|
||||
|
||||
if user.type == "corporate":
|
||||
self._set_corporate_default_groups(user)
|
||||
|
||||
if user.corporate:
|
||||
self._assign_corporate_to_user(user, code)
|
||||
|
||||
if user.groupName and len(user.groupName.strip()) > 0:
|
||||
self._assign_user_to_group_by_name(user, maker_id)
|
||||
|
||||
def _insert_new_user(self, user: UserDTO):
|
||||
new_user = {
|
||||
**user.dict(exclude={
|
||||
'passport_id', 'groupName', 'expiryDate',
|
||||
'corporate', 'passwordHash', 'passwordSalt'
|
||||
}),
|
||||
'id': str(user.id),
|
||||
'bio': "",
|
||||
'focus': "academic",
|
||||
'status': "active",
|
||||
'desiredLevels': self._DEFAULT_DESIRED_LEVELS,
|
||||
'profilePicture': "/defaultAvatar.png",
|
||||
'levels': self._DEFAULT_LEVELS,
|
||||
'isFirstLogin': False,
|
||||
'isVerified': True,
|
||||
'registrationDate': datetime.now(),
|
||||
'subscriptionExpirationDate': user.expiryDate
|
||||
}
|
||||
self._db.users.insert_one(new_user)
|
||||
|
||||
def _create_code(self, user: UserDTO, maker_id: str) -> str:
|
||||
code = shortuuid.ShortUUID().random(length=6)
|
||||
self._db.codes.insert_one({
|
||||
'id': code,
|
||||
'code': code,
|
||||
'creator': maker_id,
|
||||
'expiryDate': user.expiryDate,
|
||||
'type': user.type,
|
||||
'creationDate': datetime.now(),
|
||||
'userId': str(user.id),
|
||||
'email': user.email,
|
||||
'name': user.name,
|
||||
'passport_id': user.passport_id
|
||||
})
|
||||
return code
|
||||
|
||||
def _set_corporate_default_groups(self, user: UserDTO):
|
||||
user_id = str(user.id)
|
||||
default_groups = [
|
||||
{
|
||||
'admin': user_id,
|
||||
'id': str(uuid.uuid4()),
|
||||
'name': "Teachers",
|
||||
'participants': [],
|
||||
'disableEditing': True,
|
||||
},
|
||||
{
|
||||
'admin': user_id,
|
||||
'id': str(uuid.uuid4()),
|
||||
'name': "Students",
|
||||
'participants': [],
|
||||
'disableEditing': True,
|
||||
},
|
||||
{
|
||||
'admin': user_id,
|
||||
'id': str(uuid.uuid4()),
|
||||
'name': "Corporate",
|
||||
'participants': [],
|
||||
'disableEditing': True,
|
||||
}
|
||||
]
|
||||
for group in default_groups:
|
||||
self._db.groups.insert_one(group)
|
||||
|
||||
def _assign_corporate_to_user(self, user: UserDTO, code: str):
|
||||
user_id = str(user.id)
|
||||
corporate_user = self._db.users.find_one(
|
||||
{"email": user.corporate}
|
||||
)
|
||||
if corporate_user:
|
||||
self._db.codes.update_one(
|
||||
{"id": code},
|
||||
{"$set": {"creator": corporate_user["id"]}},
|
||||
upsert=True
|
||||
)
|
||||
group_type = "Students" if user.type == "student" else "Teachers"
|
||||
|
||||
group = self._db.groups.find_one(
|
||||
{
|
||||
"admin": corporate_user["id"],
|
||||
"name": group_type
|
||||
}
|
||||
)
|
||||
|
||||
if group:
|
||||
participants = group['participants']
|
||||
if user_id not in participants:
|
||||
participants.append(user_id)
|
||||
self._db.groups.update_one(
|
||||
{"id": group["id"]},
|
||||
{"$set": {"participants": participants}}
|
||||
)
|
||||
|
||||
else:
|
||||
group = {
|
||||
'admin': corporate_user["id"],
|
||||
'id': str(uuid.uuid4()),
|
||||
'name': group_type,
|
||||
'participants': [user_id],
|
||||
'disableEditing': True,
|
||||
}
|
||||
|
||||
self._db.groups.insert_one(group)
|
||||
|
||||
def _assign_user_to_group_by_name(self, user: UserDTO, maker_id: str):
|
||||
user_id = str(user.id)
|
||||
|
||||
groups = list(self._db.groups.find(
|
||||
{
|
||||
"admin": maker_id,
|
||||
"name": user.groupName.strip()
|
||||
}
|
||||
))
|
||||
|
||||
if len(groups) == 0:
|
||||
new_group = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'admin': maker_id,
|
||||
'name': user.groupName.strip(),
|
||||
'participants': [user_id],
|
||||
'disableEditing': False,
|
||||
}
|
||||
self._db.groups.insert_one(new_group)
|
||||
else:
|
||||
group = groups[0]
|
||||
participants = group["participants"]
|
||||
if user_id not in participants:
|
||||
participants.append(user_id)
|
||||
self._db.groups.update_one(
|
||||
{"id": group["id"]},
|
||||
{"$set": {"participants": participants}}
|
||||
)
|
||||
Reference in New Issue
Block a user