feat: add sum with image reference problem
This commit is contained in:
@@ -66,3 +66,31 @@ curl -X POST "http://127.0.0.1:8000/math/grade_1/compose_and_decompose_numbers"
|
|||||||
"seed": 1
|
"seed": 1
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sum with image reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://127.0.0.1:8000/math/grade_1/sum_with_image_reference" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"first_quantity": 5,
|
||||||
|
"second_quantity": 4,
|
||||||
|
"first_description": "flores rojas",
|
||||||
|
"second_description": "flores blancas",
|
||||||
|
"common_object": "flores",
|
||||||
|
"first_picture": {
|
||||||
|
"id": "red-flower",
|
||||||
|
"name": "Red flower",
|
||||||
|
"image_path": "/images/red-flower.png"
|
||||||
|
},
|
||||||
|
"second_picture": {
|
||||||
|
"id": "white-flower",
|
||||||
|
"name": "White flower",
|
||||||
|
"image_path": "/images/white-flower.png"
|
||||||
|
},
|
||||||
|
"option_count": 3,
|
||||||
|
"min_option_quantity": 1,
|
||||||
|
"max_option_quantity": 20,
|
||||||
|
"seed": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
92
app/problems/grade_1/sum_with_image_reference.py
Normal file
92
app/problems/grade_1/sum_with_image_reference.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
from app.schemas.grade_1.sum_with_image_reference import (
|
||||||
|
AddendGroup,
|
||||||
|
FigureGroup,
|
||||||
|
ImageOption,
|
||||||
|
PictureAsset,
|
||||||
|
SumWithImageReferenceProblem,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sum_with_image_reference(
|
||||||
|
first_quantity: int,
|
||||||
|
second_quantity: int,
|
||||||
|
first_description: str,
|
||||||
|
second_description: str,
|
||||||
|
common_object: str,
|
||||||
|
first_picture: dict,
|
||||||
|
second_picture: dict,
|
||||||
|
option_count: int = 3,
|
||||||
|
min_option_quantity: int = 1,
|
||||||
|
max_option_quantity: int = 20,
|
||||||
|
seed: int | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Generate an addition question with image answer options."""
|
||||||
|
if option_count < 2:
|
||||||
|
raise ValueError("option_count must be at least 2")
|
||||||
|
|
||||||
|
total = first_quantity + second_quantity
|
||||||
|
option_values = list(range(max(2, min_option_quantity), max_option_quantity + 1))
|
||||||
|
|
||||||
|
if total not in option_values:
|
||||||
|
raise ValueError("option quantity range must include the correct total")
|
||||||
|
|
||||||
|
distractor_values = [value for value in option_values if value != total]
|
||||||
|
if len(distractor_values) < option_count - 1:
|
||||||
|
raise ValueError("option quantity range must contain enough distractor values")
|
||||||
|
|
||||||
|
rng = random.Random(seed)
|
||||||
|
selected_values = rng.sample(distractor_values, option_count - 1) + [total]
|
||||||
|
rng.shuffle(selected_values)
|
||||||
|
first_option_picture = PictureAsset.model_validate(first_picture)
|
||||||
|
second_option_picture = PictureAsset.model_validate(second_picture)
|
||||||
|
question = (
|
||||||
|
f"Hay {first_quantity} {first_description} y "
|
||||||
|
f"{second_quantity} {second_description}, "
|
||||||
|
f"¿cuántas {common_object} hay en total?"
|
||||||
|
)
|
||||||
|
options: list[ImageOption] = []
|
||||||
|
|
||||||
|
for index, quantity in enumerate(selected_values):
|
||||||
|
if quantity == total:
|
||||||
|
option_first_quantity = first_quantity
|
||||||
|
option_second_quantity = second_quantity
|
||||||
|
else:
|
||||||
|
option_first_quantity = rng.randint(1, quantity - 1)
|
||||||
|
option_second_quantity = quantity - option_first_quantity
|
||||||
|
|
||||||
|
options.append(
|
||||||
|
ImageOption(
|
||||||
|
position=index + 1,
|
||||||
|
quantity=quantity,
|
||||||
|
first_group=FigureGroup(
|
||||||
|
quantity=option_first_quantity,
|
||||||
|
description=first_description,
|
||||||
|
picture=first_option_picture,
|
||||||
|
),
|
||||||
|
second_group=FigureGroup(
|
||||||
|
quantity=option_second_quantity,
|
||||||
|
description=second_description,
|
||||||
|
picture=second_option_picture,
|
||||||
|
),
|
||||||
|
is_correct=quantity == total,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
problem = SumWithImageReferenceProblem(
|
||||||
|
question=question,
|
||||||
|
first_group=AddendGroup(
|
||||||
|
quantity=first_quantity,
|
||||||
|
description=first_description,
|
||||||
|
),
|
||||||
|
second_group=AddendGroup(
|
||||||
|
quantity=second_quantity,
|
||||||
|
description=second_description,
|
||||||
|
),
|
||||||
|
common_object=common_object,
|
||||||
|
total=total,
|
||||||
|
options=options,
|
||||||
|
)
|
||||||
|
|
||||||
|
return problem.model_dump()
|
||||||
@@ -6,6 +6,7 @@ from app.problems.grade_1.compose_and_decompose_numbers import (
|
|||||||
from app.problems.grade_1.join_pictures_with_quantity import (
|
from app.problems.grade_1.join_pictures_with_quantity import (
|
||||||
join_pictures_with_quantity,
|
join_pictures_with_quantity,
|
||||||
)
|
)
|
||||||
|
from app.problems.grade_1.sum_with_image_reference import sum_with_image_reference
|
||||||
from app.problems.grade_1.where_are_more_items import where_are_more_items
|
from app.problems.grade_1.where_are_more_items import where_are_more_items
|
||||||
from app.schemas.grade_1.compose_and_decompose_numbers import (
|
from app.schemas.grade_1.compose_and_decompose_numbers import (
|
||||||
ComposeAndDecomposeNumbersProblem,
|
ComposeAndDecomposeNumbersProblem,
|
||||||
@@ -15,6 +16,10 @@ from app.schemas.grade_1.join_pictures_with_quantity import (
|
|||||||
JoinPicturesWithQuantityProblem,
|
JoinPicturesWithQuantityProblem,
|
||||||
JoinPicturesWithQuantityRequest,
|
JoinPicturesWithQuantityRequest,
|
||||||
)
|
)
|
||||||
|
from app.schemas.grade_1.sum_with_image_reference import (
|
||||||
|
SumWithImageReferenceProblem,
|
||||||
|
SumWithImageReferenceRequest,
|
||||||
|
)
|
||||||
from app.schemas.grade_1.where_are_more_items import (
|
from app.schemas.grade_1.where_are_more_items import (
|
||||||
WhereAreMoreItemsProblem,
|
WhereAreMoreItemsProblem,
|
||||||
WhereAreMoreItemsRequest,
|
WhereAreMoreItemsRequest,
|
||||||
@@ -62,6 +67,31 @@ def create_join_pictures_with_quantity_problem(
|
|||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/sum_with_image_reference",
|
||||||
|
response_model=SumWithImageReferenceProblem,
|
||||||
|
)
|
||||||
|
def create_sum_with_image_reference_problem(
|
||||||
|
request: SumWithImageReferenceRequest,
|
||||||
|
) -> dict:
|
||||||
|
try:
|
||||||
|
return sum_with_image_reference(
|
||||||
|
first_quantity=request.first_quantity,
|
||||||
|
second_quantity=request.second_quantity,
|
||||||
|
first_description=request.first_description,
|
||||||
|
second_description=request.second_description,
|
||||||
|
common_object=request.common_object,
|
||||||
|
first_picture=request.first_picture.model_dump(),
|
||||||
|
second_picture=request.second_picture.model_dump(),
|
||||||
|
option_count=request.option_count,
|
||||||
|
min_option_quantity=request.min_option_quantity,
|
||||||
|
max_option_quantity=request.max_option_quantity,
|
||||||
|
seed=request.seed,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/where_are_more_items",
|
"/where_are_more_items",
|
||||||
response_model=WhereAreMoreItemsProblem,
|
response_model=WhereAreMoreItemsProblem,
|
||||||
|
|||||||
51
app/schemas/grade_1/sum_with_image_reference.py
Normal file
51
app/schemas/grade_1/sum_with_image_reference.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from pydantic import BaseModel, Field, PositiveInt
|
||||||
|
|
||||||
|
|
||||||
|
class PictureAsset(BaseModel):
|
||||||
|
id: str = Field(min_length=1)
|
||||||
|
name: str = Field(min_length=1)
|
||||||
|
image_path: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class AddendGroup(BaseModel):
|
||||||
|
quantity: PositiveInt
|
||||||
|
description: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class FigureGroup(BaseModel):
|
||||||
|
quantity: PositiveInt
|
||||||
|
description: str = Field(min_length=1)
|
||||||
|
picture: PictureAsset
|
||||||
|
|
||||||
|
|
||||||
|
class ImageOption(BaseModel):
|
||||||
|
position: PositiveInt
|
||||||
|
quantity: PositiveInt
|
||||||
|
first_group: FigureGroup
|
||||||
|
second_group: FigureGroup
|
||||||
|
is_correct: bool
|
||||||
|
has_answer_box: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class SumWithImageReferenceProblem(BaseModel):
|
||||||
|
question: str
|
||||||
|
instructions: str = "Escoge la imagen en la próxima página."
|
||||||
|
first_group: AddendGroup
|
||||||
|
second_group: AddendGroup
|
||||||
|
common_object: str = Field(min_length=1)
|
||||||
|
total: PositiveInt
|
||||||
|
options: list[ImageOption]
|
||||||
|
|
||||||
|
|
||||||
|
class SumWithImageReferenceRequest(BaseModel):
|
||||||
|
first_quantity: PositiveInt
|
||||||
|
second_quantity: PositiveInt
|
||||||
|
first_description: str = Field(min_length=1)
|
||||||
|
second_description: str = Field(min_length=1)
|
||||||
|
common_object: str = Field(min_length=1)
|
||||||
|
first_picture: PictureAsset
|
||||||
|
second_picture: PictureAsset
|
||||||
|
option_count: PositiveInt = 3
|
||||||
|
min_option_quantity: PositiveInt = 1
|
||||||
|
max_option_quantity: PositiveInt = 20
|
||||||
|
seed: int | None = None
|
||||||
88
tests/test_sum_with_image_reference_endpoint.py
Normal file
88
tests/test_sum_with_image_reference_endpoint.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import create_app
|
||||||
|
|
||||||
|
|
||||||
|
class SumWithImageReferenceEndpointTest(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.client = TestClient(create_app())
|
||||||
|
self.first_picture = {
|
||||||
|
"id": "red-flower",
|
||||||
|
"name": "Red flower",
|
||||||
|
"image_path": "/images/red-flower.png",
|
||||||
|
}
|
||||||
|
self.second_picture = {
|
||||||
|
"id": "white-flower",
|
||||||
|
"name": "White flower",
|
||||||
|
"image_path": "/images/white-flower.png",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_creates_problem_with_one_correct_option(self) -> None:
|
||||||
|
response = self.client.post(
|
||||||
|
"/math/grade_1/sum_with_image_reference",
|
||||||
|
json={
|
||||||
|
"first_quantity": 5,
|
||||||
|
"second_quantity": 4,
|
||||||
|
"first_description": "flores rojas",
|
||||||
|
"second_description": "flores blancas",
|
||||||
|
"common_object": "flores",
|
||||||
|
"first_picture": self.first_picture,
|
||||||
|
"second_picture": self.second_picture,
|
||||||
|
"option_count": 3,
|
||||||
|
"seed": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
problem = response.json()
|
||||||
|
correct_options = [option for option in problem["options"] if option["is_correct"]]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
problem["question"],
|
||||||
|
"Hay 5 flores rojas y 4 flores blancas, ¿cuántas flores hay en total?",
|
||||||
|
)
|
||||||
|
self.assertEqual(problem["instructions"], "Escoge la imagen en la próxima página.")
|
||||||
|
self.assertEqual(problem["total"], 9)
|
||||||
|
self.assertEqual(len(problem["options"]), 3)
|
||||||
|
self.assertEqual(len(correct_options), 1)
|
||||||
|
self.assertEqual(correct_options[0]["quantity"], 9)
|
||||||
|
self.assertEqual(correct_options[0]["first_group"]["quantity"], 5)
|
||||||
|
self.assertEqual(correct_options[0]["second_group"]["quantity"], 4)
|
||||||
|
|
||||||
|
for option in problem["options"]:
|
||||||
|
first_group = option["first_group"]
|
||||||
|
second_group = option["second_group"]
|
||||||
|
|
||||||
|
self.assertEqual(option["quantity"], first_group["quantity"] + second_group["quantity"])
|
||||||
|
self.assertEqual(first_group["description"], "flores rojas")
|
||||||
|
self.assertEqual(second_group["description"], "flores blancas")
|
||||||
|
self.assertEqual(first_group["picture"], self.first_picture)
|
||||||
|
self.assertEqual(second_group["picture"], self.second_picture)
|
||||||
|
|
||||||
|
def test_returns_bad_request_when_total_is_outside_option_range(self) -> None:
|
||||||
|
response = self.client.post(
|
||||||
|
"/math/grade_1/sum_with_image_reference",
|
||||||
|
json={
|
||||||
|
"first_quantity": 5,
|
||||||
|
"second_quantity": 4,
|
||||||
|
"first_description": "flores rojas",
|
||||||
|
"second_description": "flores blancas",
|
||||||
|
"common_object": "flores",
|
||||||
|
"first_picture": self.first_picture,
|
||||||
|
"second_picture": self.second_picture,
|
||||||
|
"min_option_quantity": 1,
|
||||||
|
"max_option_quantity": 8,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(
|
||||||
|
response.json(),
|
||||||
|
{"detail": "option quantity range must include the correct total"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user