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
|
||||
}'
|
||||
```
|
||||
|
||||
### 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 (
|
||||
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.schemas.grade_1.compose_and_decompose_numbers import (
|
||||
ComposeAndDecomposeNumbersProblem,
|
||||
@@ -15,6 +16,10 @@ from app.schemas.grade_1.join_pictures_with_quantity import (
|
||||
JoinPicturesWithQuantityProblem,
|
||||
JoinPicturesWithQuantityRequest,
|
||||
)
|
||||
from app.schemas.grade_1.sum_with_image_reference import (
|
||||
SumWithImageReferenceProblem,
|
||||
SumWithImageReferenceRequest,
|
||||
)
|
||||
from app.schemas.grade_1.where_are_more_items import (
|
||||
WhereAreMoreItemsProblem,
|
||||
WhereAreMoreItemsRequest,
|
||||
@@ -62,6 +67,31 @@ def create_join_pictures_with_quantity_problem(
|
||||
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(
|
||||
"/where_are_more_items",
|
||||
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