Merged in release/async (pull request #43)
ENCOA-255 gpt was grouping parts by sections and the reading passages were not updated with text.content instead of the old context field Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -54,7 +54,6 @@ async def import_level(
|
|||||||
):
|
):
|
||||||
return await level_controller.upload_level(exercises, solutions)
|
return await level_controller.upload_level(exercises, solutions)
|
||||||
|
|
||||||
|
|
||||||
@level_router.post(
|
@level_router.post(
|
||||||
'/custom/',
|
'/custom/',
|
||||||
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class ReadingExerciseType(str, Enum):
|
|||||||
trueFalse = "trueFalse"
|
trueFalse = "trueFalse"
|
||||||
paragraphMatch = "paragraphMatch"
|
paragraphMatch = "paragraphMatch"
|
||||||
ideaMatch = "ideaMatch"
|
ideaMatch = "ideaMatch"
|
||||||
|
multipleChoice = "multipleChoice"
|
||||||
|
|
||||||
|
|
||||||
class ListeningExerciseType(str, Enum):
|
class ListeningExerciseType(str, Enum):
|
||||||
|
|||||||
@@ -47,10 +47,13 @@ class FillBlanksExercise(BaseModel):
|
|||||||
|
|
||||||
Exercise = Union[MultipleChoiceExercise, FillBlanksExercise]
|
Exercise = Union[MultipleChoiceExercise, FillBlanksExercise]
|
||||||
|
|
||||||
|
class Text(BaseModel):
|
||||||
|
content: str
|
||||||
|
title: str
|
||||||
|
|
||||||
class Part(BaseModel):
|
class Part(BaseModel):
|
||||||
exercises: List[Exercise]
|
exercises: List[Exercise]
|
||||||
context: Optional[str] = Field(default=None)
|
text: Optional[Text] = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class Exam(BaseModel):
|
class Exam(BaseModel):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pydantic import ValidationError
|
|||||||
from app.dtos.exams.level import (
|
from app.dtos.exams.level import (
|
||||||
MultipleChoiceExercise,
|
MultipleChoiceExercise,
|
||||||
FillBlanksExercise,
|
FillBlanksExercise,
|
||||||
Part, Exam
|
Part, Exam, Text
|
||||||
)
|
)
|
||||||
from app.dtos.sheet import Sheet, Option, MultipleChoiceQuestion, FillBlanksWord
|
from app.dtos.sheet import Sheet, Option, MultipleChoiceQuestion, FillBlanksWord
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ class LevelMapper:
|
|||||||
parts = []
|
parts = []
|
||||||
for part in response['parts']:
|
for part in response['parts']:
|
||||||
part_exercises = part['exercises']
|
part_exercises = part['exercises']
|
||||||
context = part.get('context', None)
|
text = part.get('text', None)
|
||||||
|
|
||||||
exercises = []
|
exercises = []
|
||||||
for exercise in part_exercises:
|
for exercise in part_exercises:
|
||||||
@@ -32,8 +32,13 @@ class LevelMapper:
|
|||||||
exercises.append(exercise_model)
|
exercises.append(exercise_model)
|
||||||
|
|
||||||
part_kwargs = {"exercises": exercises}
|
part_kwargs = {"exercises": exercises}
|
||||||
if context is not None:
|
if text is not None and text.get('content', None):
|
||||||
part_kwargs["context"] = context
|
title = text.get('title', 'Untitled')
|
||||||
|
if title == '':
|
||||||
|
title = 'Untitled'
|
||||||
|
part_kwargs["text"] = Text(title=title, content=text['content'])
|
||||||
|
else:
|
||||||
|
part_kwargs["text"] = None
|
||||||
|
|
||||||
part_model = Part(**part_kwargs)
|
part_model = Part(**part_kwargs)
|
||||||
parts.append(part_model)
|
parts.append(part_model)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class UploadLevelModule:
|
|||||||
#completion: Coroutine[Any, Any, Exam] = (
|
#completion: Coroutine[Any, Any, Exam] = (
|
||||||
# self._png_completion(path_id) if file_has_images else self._html_completion(path_id)
|
# self._png_completion(path_id) if file_has_images else self._html_completion(path_id)
|
||||||
#)
|
#)
|
||||||
response = await self._html_completion(path_id)
|
response = await self._html_completion(path_id, solutions is not None)
|
||||||
|
|
||||||
FileHelper.remove_directory(f'./tmp/{path_id}')
|
FileHelper.remove_directory(f'./tmp/{path_id}')
|
||||||
|
|
||||||
@@ -57,7 +57,10 @@ class UploadLevelModule:
|
|||||||
return {
|
return {
|
||||||
"parts": [
|
"parts": [
|
||||||
{
|
{
|
||||||
"context": "<this attribute is optional you may exclude it if not required>",
|
"text": {
|
||||||
|
"content": "<this attribute is mandatory if there is a text passage else this 'text' field is omitted>",
|
||||||
|
"title": "<this attribute is optional you may exclude it if not required>",
|
||||||
|
},
|
||||||
"exercises": [
|
"exercises": [
|
||||||
self._multiple_choice_html(),
|
self._multiple_choice_html(),
|
||||||
self._passage_blank_space_html()
|
self._passage_blank_space_html()
|
||||||
@@ -66,16 +69,26 @@ class UploadLevelModule:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _html_completion(self, path_id: str) -> Exam:
|
async def _html_completion(self, path_id: str, solutions_provided: bool) -> Exam:
|
||||||
async with aiofiles.open(f'./tmp/{path_id}/exercises.html', 'r', encoding='utf-8') as f:
|
async with aiofiles.open(f'./tmp/{path_id}/exercises.html', 'r', encoding='utf-8') as f:
|
||||||
html = await f.read()
|
html = await f.read()
|
||||||
|
|
||||||
|
solutions = []
|
||||||
|
if solutions_provided:
|
||||||
|
async with aiofiles.open(f'./tmp/{path_id}/solutions.html', 'r', encoding='utf-8') as f:
|
||||||
|
solutions_html = await f.read()
|
||||||
|
solutions.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": f'The solutions to the question sheet are the following:\n\n{solutions_html}'
|
||||||
|
})
|
||||||
|
|
||||||
return await self._llm.pydantic_prediction(
|
return await self._llm.pydantic_prediction(
|
||||||
[self._gpt_instructions_html(),
|
[self._gpt_instructions_html(),
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": html
|
"content": html
|
||||||
}
|
},
|
||||||
|
*solutions
|
||||||
],
|
],
|
||||||
LevelMapper.map_to_exam_model,
|
LevelMapper.map_to_exam_model,
|
||||||
str(self._level_json_schema())
|
str(self._level_json_schema())
|
||||||
@@ -86,7 +99,7 @@ class UploadLevelModule:
|
|||||||
"role": "system",
|
"role": "system",
|
||||||
"content": (
|
"content": (
|
||||||
'You are GPT Scraper and your job is to clean dirty html into clean usable JSON formatted data.'
|
'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'
|
'Your current task is to scrape html english questions sheets and structure them into parts NOT sections.\n\n'
|
||||||
|
|
||||||
'In the question sheet you will only see 4 types of question:\n'
|
'In the question sheet you will only see 4 types of question:\n'
|
||||||
'- blank space multiple choice\n'
|
'- blank space multiple choice\n'
|
||||||
@@ -111,23 +124,26 @@ class UploadLevelModule:
|
|||||||
'out the best paragraph separation possible.'
|
'out the best paragraph separation possible.'
|
||||||
|
|
||||||
'You will place all the information in a single JSON: '
|
'You will place all the information in a single JSON: '
|
||||||
'{"parts": [{"exercises": [{...}], "context": ""}]}\n '
|
'{"parts": [{"exercises": [{...}], "text": {"title": "", "content": ""} ]}\n '
|
||||||
'Where {...} are the exercises templates for each part of a question sheet and the optional field '
|
'Where {...} are the exercises templates for each part of a question sheet and the optional field '
|
||||||
'context.'
|
'text, which contains the reading passages that are required in order to solve the part questions, '
|
||||||
|
'(if there are passages) place them in text.content and if there is a title place it in text.title '
|
||||||
|
'else omit the title field.\n'
|
||||||
|
|
||||||
'IMPORTANT: The question sheet may be divided by sections but you need to only consider the parts, '
|
'IMPORTANT: As stated earlier your job is to structure the questions into PARTS not SECTION, this means '
|
||||||
'so that you can group the exercises by the parts that are in the html, this is crucial since only '
|
'that if there is for example: Section 1, Part 1 and Part 2, Section 2, Part 1 and Part 2, you MUST '
|
||||||
'reading passage multiple choice require context and if the context is included in parts where it '
|
'place in the parts array 4 parts NOT 2 parts with the exercises of both parts! If there are no sections '
|
||||||
'is not required the UI will be messed up. Some make sure to correctly group the exercises by parts.\n'
|
'and only Parts then group them by parts, and when I say parts I mean it in the fucking literal sense of the'
|
||||||
|
' word Part x which is in the html. '
|
||||||
|
'You must strictly adhere to this instruction, do not mistake sections for parts!\n'
|
||||||
|
|
||||||
'The templates for the exercises are the following:\n'
|
'The templates for the exercises are the following:\n'
|
||||||
'- blank space multiple choice, underline multiple choice and reading passage multiple choice: '
|
'- blank space multiple choice, underline multiple choice and reading passage multiple choice: '
|
||||||
f'{self._multiple_choice_html()}\n'
|
f'{self._multiple_choice_html()}\n'
|
||||||
f'- reading passage blank space multiple choice: {self._passage_blank_space_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 '
|
'IMPORTANT: The text.content field must be set with the reading passages of a part (if there is one)'
|
||||||
'passages without paragraphs or line numbers, with 2 newlines between paragraphs, for the other '
|
'without paragraphs or line numbers, with 2 newlines between paragraphs.'
|
||||||
'exercises exclude the context field.'
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,30 +151,19 @@ class UploadLevelModule:
|
|||||||
def _multiple_choice_html():
|
def _multiple_choice_html():
|
||||||
return {
|
return {
|
||||||
"type": "multipleChoice",
|
"type": "multipleChoice",
|
||||||
"prompt": "Select the appropriate option.",
|
"prompt": "<general instructions for this section>",
|
||||||
"questions": [
|
"questions": [
|
||||||
{
|
{
|
||||||
"id": "<the question id>",
|
"id": "<question number as string>",
|
||||||
"prompt": "<the question>",
|
"prompt": "<question text>",
|
||||||
"solution": "<the option id solution>",
|
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"id": "A",
|
"id": "<A/B/C/D>",
|
||||||
"text": "<the a option>"
|
"text": "<option text>"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "B",
|
|
||||||
"text": "<the b option>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "C",
|
|
||||||
"text": "<the c option>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "D",
|
|
||||||
"text": "<the d option>"
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"solution": "<correct option letter>",
|
||||||
|
"variant": "text"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -171,17 +176,17 @@ class UploadLevelModule:
|
|||||||
"prompt": "Click a blank to select the appropriate word for it.",
|
"prompt": "Click a blank to select the appropriate word for it.",
|
||||||
"text": (
|
"text": (
|
||||||
"<The whole text for the exercise with replacements for blank spaces and their "
|
"<The whole text for the exercise with replacements for blank spaces and their "
|
||||||
"ids with {{<question id>}} with 2 newlines between paragraphs>"
|
"ids with {{<question id/number>}} with 2 newlines between paragraphs>"
|
||||||
),
|
),
|
||||||
"solutions": [
|
"solutions": [
|
||||||
{
|
{
|
||||||
"id": "<question id>",
|
"id": "<question number>",
|
||||||
"solution": "<the option that holds the solution>"
|
"solution": "<the option that holds the solution>"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"words": [
|
"words": [
|
||||||
{
|
{
|
||||||
"id": "<question id>",
|
"id": "<question number>",
|
||||||
"options": {
|
"options": {
|
||||||
"A": "<a option>",
|
"A": "<a option>",
|
||||||
"B": "<b option>",
|
"B": "<b option>",
|
||||||
@@ -205,7 +210,7 @@ class UploadLevelModule:
|
|||||||
self._multiple_choice_png(),
|
self._multiple_choice_png(),
|
||||||
{"type": "blanksPassage", "text": (
|
{"type": "blanksPassage", "text": (
|
||||||
"<The whole text for the exercise with replacements for blank spaces and their "
|
"<The whole text for the exercise with replacements for blank spaces and their "
|
||||||
"ids with {{<question id>}} with 2 newlines between paragraphs>"
|
"ids with {{<question number>}} with 2 newlines between paragraphs>"
|
||||||
)},
|
)},
|
||||||
{"type": "passage", "context": (
|
{"type": "passage", "context": (
|
||||||
"<reading passages without paragraphs or line numbers, with 2 newlines between paragraphs>"
|
"<reading passages without paragraphs or line numbers, with 2 newlines between paragraphs>"
|
||||||
|
|||||||
@@ -14,11 +14,10 @@ from app.configs.constants import (
|
|||||||
)
|
)
|
||||||
from app.helpers import FileHelper
|
from app.helpers import FileHelper
|
||||||
from .import_listening import ImportListeningModule
|
from .import_listening import ImportListeningModule
|
||||||
from .multiple_choice import MultipleChoice
|
|
||||||
from .write_blank_forms import WriteBlankForms
|
from .write_blank_forms import WriteBlankForms
|
||||||
from .write_blanks import WriteBlanks
|
from .write_blanks import WriteBlanks
|
||||||
from .write_blank_notes import WriteBlankNotes
|
from .write_blank_notes import WriteBlankNotes
|
||||||
from ..shared import TrueFalse
|
from ..shared import TrueFalse, MultipleChoice
|
||||||
|
|
||||||
|
|
||||||
class ListeningService(IListeningService):
|
class ListeningService(IListeningService):
|
||||||
@@ -128,7 +127,7 @@ class ListeningService(IListeningService):
|
|||||||
if req_exercise.type == "multipleChoice" or req_exercise.type == "multipleChoice3Options":
|
if req_exercise.type == "multipleChoice" or req_exercise.type == "multipleChoice3Options":
|
||||||
n_options = 4 if req_exercise.type == "multipleChoice" else 3
|
n_options = 4 if req_exercise.type == "multipleChoice" else 3
|
||||||
question = await self._multiple_choice.gen_multiple_choice(
|
question = await self._multiple_choice.gen_multiple_choice(
|
||||||
dialog_type, text, req_exercise.quantity, start_id, difficulty, n_options
|
text, req_exercise.quantity, start_id, difficulty, n_options
|
||||||
)
|
)
|
||||||
self._logger.info(f"Added multiple choice: {question}")
|
self._logger.info(f"Added multiple choice: {question}")
|
||||||
return question
|
return question
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from app.services.abc import IReadingService, ILLMService
|
|||||||
from .fill_blanks import FillBlanks
|
from .fill_blanks import FillBlanks
|
||||||
from .idea_match import IdeaMatch
|
from .idea_match import IdeaMatch
|
||||||
from .paragraph_match import ParagraphMatch
|
from .paragraph_match import ParagraphMatch
|
||||||
from ..shared import TrueFalse
|
from ..shared import TrueFalse, MultipleChoice
|
||||||
from .import_reading import ImportReadingModule
|
from .import_reading import ImportReadingModule
|
||||||
from .write_blanks import WriteBlanks
|
from .write_blanks import WriteBlanks
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ class ReadingService(IReadingService):
|
|||||||
self._paragraph_match = ParagraphMatch(llm)
|
self._paragraph_match = ParagraphMatch(llm)
|
||||||
self._true_false = TrueFalse(llm)
|
self._true_false = TrueFalse(llm)
|
||||||
self._write_blanks = WriteBlanks(llm)
|
self._write_blanks = WriteBlanks(llm)
|
||||||
|
self._multiple_choice = MultipleChoice(llm)
|
||||||
self._logger = getLogger(__name__)
|
self._logger = getLogger(__name__)
|
||||||
self._import = ImportReadingModule(llm)
|
self._import = ImportReadingModule(llm)
|
||||||
|
|
||||||
@@ -119,6 +120,12 @@ class ReadingService(IReadingService):
|
|||||||
question["variant"] = "ideaMatch"
|
question["variant"] = "ideaMatch"
|
||||||
self._logger.info(f"Added idea match: {question}")
|
self._logger.info(f"Added idea match: {question}")
|
||||||
return question
|
return question
|
||||||
|
elif req_exercise.type == "multipleChoice":
|
||||||
|
question = await self._multiple_choice.gen_multiple_choice(
|
||||||
|
text, req_exercise.quantity, start_id, difficulty, 4
|
||||||
|
)
|
||||||
|
self._logger.info(f"Added multiple choice: {question}")
|
||||||
|
return question
|
||||||
|
|
||||||
async def generate_reading_exercises(self, dto: ReadingDTO):
|
async def generate_reading_exercises(self, dto: ReadingDTO):
|
||||||
exercise_tasks = []
|
exercise_tasks = []
|
||||||
|
|||||||
@@ -98,7 +98,11 @@ class ImportReadingModule:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"text": "<numbered questions with format in square brackets: [<question text>{{<question number>}}\\\\n] notice how there is a double backslash before the n -> I want an escaped newline in your output> ",
|
"text": (
|
||||||
|
"<numbered questions with format in square brackets: [<question text>{{<question number>}}\\\\n] "
|
||||||
|
"- notice how there the question number inside {{}} -> the text MUST always contain the question number in that format "
|
||||||
|
"- and notice how there is a double backslash before the n -> I want an escaped newline in your output> "
|
||||||
|
),
|
||||||
"type": "writeBlanks",
|
"type": "writeBlanks",
|
||||||
"prompt": "<specific instructions for this exercise section>"
|
"prompt": "<specific instructions for this exercise section>"
|
||||||
}
|
}
|
||||||
@@ -192,13 +196,14 @@ class ImportReadingModule:
|
|||||||
+ (
|
+ (
|
||||||
"Solutions were not provided - analyze the passage carefully to determine correct answers."
|
"Solutions were not provided - analyze the passage carefully to determine correct answers."
|
||||||
if not solutions else
|
if not solutions else
|
||||||
"Use the provided solutions to fill in all answer fields accurately."
|
"Use the provided solutions to fill in all answer fields accurately, if word answers have all letters "
|
||||||
|
"uppercase convert them to lowercase before assigning them."
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
"Pay extra attention to fillblanks exercises the solution and option wording must match in case!"
|
"Pay extra attention to fillblanks exercises the solution and option wording must match in case! "
|
||||||
"There can't be options in lowercase and solutions in uppercase!"
|
"There can't be options in lowercase and solutions in uppercase! "
|
||||||
"Also PAY ATTENTION TO SECTIONS, these most likely indicate parts, and in each section/part there "
|
"Also PAY ATTENTION TO SECTIONS, these most likely indicate parts, and in each section/part there "
|
||||||
"should be a text, if there isn't a title for it choose a reasonable one based on its contents."
|
"should be a text, if there isn't a title for it choose a reasonable one based on its contents. "
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from .true_false import TrueFalse
|
from .true_false import TrueFalse
|
||||||
|
from .multiple_choice import MultipleChoice
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"TrueFalse"
|
"TrueFalse",
|
||||||
|
"MultipleChoice"
|
||||||
]
|
]
|
||||||
@@ -11,7 +11,7 @@ class MultipleChoice:
|
|||||||
self._llm = llm
|
self._llm = llm
|
||||||
|
|
||||||
async def gen_multiple_choice(
|
async def gen_multiple_choice(
|
||||||
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str, n_options: int = 4
|
self, text: str, quantity: int, start_id: int, difficulty: str, n_options: int = 4
|
||||||
):
|
):
|
||||||
messages = [
|
messages = [
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@ class MultipleChoice:
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": (
|
"content": (
|
||||||
f'Generate {quantity} {difficulty} difficulty multiple choice questions of {n_options} '
|
f'Generate {quantity} {difficulty} difficulty multiple choice questions of {n_options} '
|
||||||
f'options for this {dialog_type}:\n"' + text + '"')
|
f'options for this text:\n"' + text + '"')
|
||||||
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user