feat: add sum with image reference problem

This commit is contained in:
AlanSilvaaa
2026-05-26 11:56:41 -04:00
parent 29754b4782
commit c2613cb35e
6 changed files with 289 additions and 0 deletions

View File

@@ -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

View 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()

View File

@@ -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,

View 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

View 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()