refactor: implement core and server dir to split the library and endpoints for testing

This commit is contained in:
AlanSilvaaa
2026-05-29 17:43:22 -04:00
parent da59c4e64d
commit 4e9f3c164f
36 changed files with 723 additions and 58 deletions

View File

@@ -0,0 +1,17 @@
from math_problems_structure.core.problems.grade_1 import (
compose_and_decompose_numbers,
join_corresponding_sums,
join_pictures_with_quantity,
subtract_with_image_reference,
sum_with_image_reference,
where_are_more_items,
)
__all__ = [
"compose_and_decompose_numbers",
"join_corresponding_sums",
"join_pictures_with_quantity",
"subtract_with_image_reference",
"sum_with_image_reference",
"where_are_more_items",
]

View File

@@ -0,0 +1,27 @@
from math_problems_structure.core.problems.grade_1.compose_and_decompose_numbers import (
compose_and_decompose_numbers,
)
from math_problems_structure.core.problems.grade_1.join_corresponding_sums import (
join_corresponding_sums,
)
from math_problems_structure.core.problems.grade_1.join_pictures_with_quantity import (
join_pictures_with_quantity,
)
from math_problems_structure.core.problems.grade_1.subtract_with_image_reference import (
subtract_with_image_reference,
)
from math_problems_structure.core.problems.grade_1.sum_with_image_reference import (
sum_with_image_reference,
)
from math_problems_structure.core.problems.grade_1.where_are_more_items import (
where_are_more_items,
)
__all__ = [
"compose_and_decompose_numbers",
"join_corresponding_sums",
"join_pictures_with_quantity",
"subtract_with_image_reference",
"sum_with_image_reference",
"where_are_more_items",
]

View File

@@ -0,0 +1,42 @@
import random
from math_problems_structure.core.schemas.grade_1.compose_and_decompose_numbers import (
ComposeAndDecomposeNumbersProblem,
DecompositionRow,
PictureAsset,
)
def compose_and_decompose_numbers(
picture: dict,
whole: int = 10,
randomize_rows: bool = False,
seed: int | None = None,
) -> dict:
"""Generate number decomposition rows for one picture asset."""
if whole < 2:
raise ValueError("whole must be at least 2")
selected_picture = PictureAsset.model_validate(picture)
pairs = [(first_part, whole - first_part) for first_part in range(whole - 1, 0, -1)]
if randomize_rows:
rng = random.Random(seed)
rng.shuffle(pairs)
problem = ComposeAndDecomposeNumbersProblem(
whole=whole,
picture=selected_picture,
rows=[
DecompositionRow(
position=index + 1,
whole=whole,
first_part=first_part,
second_part=second_part,
picture=selected_picture,
)
for index, (first_part, second_part) in enumerate(pairs)
],
)
return problem.model_dump()

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

View File

@@ -0,0 +1,96 @@
import random
from math_problems_structure.core.schemas.grade_1.join_corresponding_sums import (
AdditionExpression,
JoinCorrespondingSumsProblem,
SumConnection,
)
def join_corresponding_sums(
pair_count: int = 3,
min_sum: int = 2,
max_sum: int = 10,
min_addend: int = 1,
max_addend: int = 9,
seed: int | None = None,
) -> dict:
"""Generate addition expressions that students connect by equal sums."""
if pair_count < 1:
raise ValueError("pair_count must be at least 1")
if min_sum > max_sum:
raise ValueError("min_sum must be less than or equal to max_sum")
if min_addend > max_addend:
raise ValueError("min_addend must be less than or equal to max_addend")
expressions_by_total: dict[int, list[tuple[int, int]]] = {}
for total in range(min_sum, max_sum + 1):
expressions = []
for first_addend in range(min_addend, max_addend + 1):
second_addend = total - first_addend
if min_addend <= second_addend <= max_addend:
expressions.append((first_addend, second_addend))
if len(expressions) >= 2:
expressions_by_total[total] = expressions
available_totals = list(expressions_by_total)
if len(available_totals) < pair_count:
raise ValueError("sum and addend ranges must contain enough matchable sums")
rng = random.Random(seed)
selected_totals = rng.sample(available_totals, pair_count)
left_items: list[dict] = []
right_items: list[dict] = []
for total in selected_totals:
left_expression, right_expression = rng.sample(expressions_by_total[total], 2)
match_id = f"sum-{total}"
left_items.append(
{
"first_addend": left_expression[0],
"second_addend": left_expression[1],
"total": total,
"match_id": match_id,
}
)
right_items.append(
{
"first_addend": right_expression[0],
"second_addend": right_expression[1],
"total": total,
"match_id": match_id,
}
)
rng.shuffle(left_items)
rng.shuffle(right_items)
left_expressions = [
AdditionExpression(position=index + 1, **item)
for index, item in enumerate(left_items)
]
right_expressions = [
AdditionExpression(position=index + 1, **item)
for index, item in enumerate(right_items)
]
right_positions_by_match_id = {
expression.match_id: expression.position for expression in right_expressions
}
answer_key = [
SumConnection(
match_id=expression.match_id,
total=expression.total,
left_position=expression.position,
right_position=right_positions_by_match_id[expression.match_id],
)
for expression in left_expressions
]
problem = JoinCorrespondingSumsProblem(
left_expressions=left_expressions,
right_expressions=right_expressions,
answer_key=answer_key,
)
return problem.model_dump()

View File

@@ -0,0 +1,73 @@
import random
from math_problems_structure.core.schemas.grade_1.join_pictures_with_quantity import (
JoinPicturesWithQuantityProblem,
NumberCard,
PictureAsset,
PictureContainer,
)
def join_pictures_with_quantity(
available_pictures: list[dict],
container_count_per_side: int = 5,
min_quantity: int = 1,
max_quantity: int = 10,
seed: int | None = None,
) -> dict:
"""Generate a count-and-match picture problem.
The caller only provides the picture assets that exist in the project. The
function chooses unique quantities, assigns pictures to the left and right
containers, and returns a JSON-ready validated problem structure.
Each picture must have: id, name, image_path.
"""
if seed is not None:
random.seed(seed)
pictures = [PictureAsset.model_validate(picture) for picture in available_pictures]
available_quantities = list(range(min_quantity, max_quantity + 1))
if container_count_per_side < 1:
raise ValueError("container_count_per_side must be at least 1")
if len(available_quantities) < container_count_per_side:
raise ValueError(
"quantity range must contain at least container_count_per_side values"
)
required_picture_count = container_count_per_side * 2
if len(pictures) < required_picture_count:
raise ValueError(
f"available_pictures must contain at least {required_picture_count} pictures"
)
number_card_values = random.sample(available_quantities, container_count_per_side)
left_quantities = random.sample(number_card_values, len(number_card_values))
right_quantities = random.sample(number_card_values, len(number_card_values))
selected_pictures = random.sample(pictures, required_picture_count)
left_pictures = selected_pictures[:container_count_per_side]
right_pictures = selected_pictures[container_count_per_side:]
problem = JoinPicturesWithQuantityProblem(
left_containers=[
PictureContainer(
position=index + 1, quantity=quantity, picture=left_pictures[index]
)
for index, quantity in enumerate(left_quantities)
],
number_cards=[
NumberCard(position=index + 1, value=value)
for index, value in enumerate(number_card_values)
],
right_containers=[
PictureContainer(
position=index + 1, quantity=quantity, picture=right_pictures[index]
)
for index, quantity in enumerate(right_quantities)
],
)
return problem.model_dump()

View File

@@ -0,0 +1,49 @@
from math_problems_structure.core.schemas.grade_1.subtract_with_image_reference import (
FigureGroup,
PictureAsset,
SubtractWithImageReferenceProblem,
SubtractionEquation,
)
def subtract_with_image_reference(
object_name: str,
initial_quantity: int,
removed_quantity: int,
figure: dict,
actor_name: str = "Diego",
) -> dict:
"""Generate a subtraction story problem with remaining and removed groups."""
if removed_quantity > initial_quantity:
raise ValueError("removed_quantity must be less than or equal to initial_quantity")
selected_figure = PictureAsset.model_validate(figure)
remaining_quantity = initial_quantity - removed_quantity
question = (
f"Hay {initial_quantity} {object_name}. "
f"{actor_name} sacó {removed_quantity} {object_name}. "
f"¿Cuántos {object_name} quedan?"
)
problem = SubtractWithImageReferenceProblem(
question=question,
object_name=object_name,
actor_name=actor_name,
figure=selected_figure,
remaining_group=FigureGroup(
label="quedan",
quantity=remaining_quantity,
picture=selected_figure,
),
subtracted_group=FigureGroup(
label="sacó",
quantity=removed_quantity,
picture=selected_figure,
),
equation=SubtractionEquation(
initial_quantity=initial_quantity,
removed_quantity=removed_quantity,
remaining_quantity=remaining_quantity,
),
)
return problem.model_dump()

View File

@@ -0,0 +1,92 @@
import random
from math_problems_structure.core.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

@@ -0,0 +1,55 @@
import random
from math_problems_structure.core.schemas.grade_1.where_are_more_items import (
ItemGroup,
MoreItemsComparison,
PictureAsset,
WhereAreMoreItemsProblem,
)
def where_are_more_items(
available_pictures: list[dict],
comparison_count: int = 3,
min_quantity: int = 1,
max_quantity: int = 10,
seed: int | None = None,
) -> dict:
"""Generate comparison cards where students choose the group with more items."""
pictures = [PictureAsset.model_validate(picture) for picture in available_pictures]
if len(pictures) < 2:
raise ValueError("available_pictures must contain at least 2 pictures")
if min_quantity >= max_quantity:
raise ValueError("max_quantity must be greater than min_quantity")
rng = random.Random(seed)
quantity_values = list(range(min_quantity, max_quantity + 1))
comparisons: list[MoreItemsComparison] = []
for index in range(comparison_count):
left_picture, right_picture = rng.sample(pictures, 2)
left_quantity, right_quantity = rng.sample(quantity_values, 2)
answer_side = "left" if left_quantity > right_quantity else "right"
comparisons.append(
MoreItemsComparison(
position=index + 1,
left_group=ItemGroup(
side="left",
picture=left_picture,
quantity=left_quantity,
),
right_group=ItemGroup(
side="right",
picture=right_picture,
quantity=right_quantity,
),
answer_side=answer_side,
)
)
problem = WhereAreMoreItemsProblem(comparisons=comparisons)
return problem.model_dump()

View File

@@ -0,0 +1,39 @@
from math_problems_structure.core.schemas.grade_1.compose_and_decompose_numbers import (
ComposeAndDecomposeNumbersProblem,
ComposeAndDecomposeNumbersRequest,
)
from math_problems_structure.core.schemas.grade_1.join_corresponding_sums import (
JoinCorrespondingSumsProblem,
JoinCorrespondingSumsRequest,
)
from math_problems_structure.core.schemas.grade_1.join_pictures_with_quantity import (
JoinPicturesWithQuantityProblem,
JoinPicturesWithQuantityRequest,
)
from math_problems_structure.core.schemas.grade_1.subtract_with_image_reference import (
SubtractWithImageReferenceProblem,
SubtractWithImageReferenceRequest,
)
from math_problems_structure.core.schemas.grade_1.sum_with_image_reference import (
SumWithImageReferenceProblem,
SumWithImageReferenceRequest,
)
from math_problems_structure.core.schemas.grade_1.where_are_more_items import (
WhereAreMoreItemsProblem,
WhereAreMoreItemsRequest,
)
__all__ = [
"ComposeAndDecomposeNumbersProblem",
"ComposeAndDecomposeNumbersRequest",
"JoinCorrespondingSumsProblem",
"JoinCorrespondingSumsRequest",
"JoinPicturesWithQuantityProblem",
"JoinPicturesWithQuantityRequest",
"SubtractWithImageReferenceProblem",
"SubtractWithImageReferenceRequest",
"SumWithImageReferenceProblem",
"SumWithImageReferenceRequest",
"WhereAreMoreItemsProblem",
"WhereAreMoreItemsRequest",
]

View File

@@ -0,0 +1,30 @@
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 DecompositionRow(BaseModel):
position: PositiveInt
whole: PositiveInt
first_part: PositiveInt
second_part: PositiveInt
picture: PictureAsset
has_answer_boxes: bool = True
class ComposeAndDecomposeNumbersProblem(BaseModel):
instructions: str = "Compón y descompón el número."
whole: PositiveInt
picture: PictureAsset
rows: list[DecompositionRow]
class ComposeAndDecomposeNumbersRequest(BaseModel):
picture: PictureAsset
whole: PositiveInt = 10
randomize_rows: bool = False
seed: int | None = None

View File

@@ -0,0 +1,32 @@
from pydantic import BaseModel, Field, PositiveInt
class AdditionExpression(BaseModel):
position: PositiveInt
first_addend: PositiveInt
second_addend: PositiveInt
total: PositiveInt
match_id: str = Field(min_length=1)
class SumConnection(BaseModel):
match_id: str = Field(min_length=1)
total: PositiveInt
left_position: PositiveInt
right_position: PositiveInt
class JoinCorrespondingSumsProblem(BaseModel):
instructions: str = "Conecta."
left_expressions: list[AdditionExpression]
right_expressions: list[AdditionExpression]
answer_key: list[SumConnection]
class JoinCorrespondingSumsRequest(BaseModel):
pair_count: PositiveInt = 3
min_sum: PositiveInt = 2
max_sum: PositiveInt = 10
min_addend: PositiveInt = 1
max_addend: PositiveInt = 9
seed: int | None = None

View File

@@ -0,0 +1,35 @@
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 PictureContainer(BaseModel):
position: PositiveInt
quantity: PositiveInt
picture: PictureAsset
class NumberCard(BaseModel):
position: PositiveInt
value: PositiveInt
shows_blocks: bool = True
has_answer_box: bool = True
class JoinPicturesWithQuantityProblem(BaseModel):
instructions: str = "Cuenta las imágenes y une cada grupo con el número correcto."
left_containers: list[PictureContainer]
number_cards: list[NumberCard]
right_containers: list[PictureContainer]
class JoinPicturesWithQuantityRequest(BaseModel):
available_pictures: list[PictureAsset] = Field(min_length=1)
container_count_per_side: PositiveInt = 5
min_quantity: PositiveInt = 1
max_quantity: PositiveInt = 10
seed: int | None = None

View File

@@ -0,0 +1,40 @@
from pydantic import BaseModel, Field, NonNegativeInt, PositiveInt
class PictureAsset(BaseModel):
id: str = Field(min_length=1)
name: str = Field(min_length=1)
image_path: str = Field(min_length=1)
class SubtractionEquation(BaseModel):
initial_quantity: PositiveInt
removed_quantity: PositiveInt
remaining_quantity: NonNegativeInt
symbol: str = "-"
equals_symbol: str = "="
class FigureGroup(BaseModel):
label: str = Field(min_length=1)
quantity: NonNegativeInt
picture: PictureAsset
class SubtractWithImageReferenceProblem(BaseModel):
title: str = "¿Cuántos quedan?"
question: str
object_name: str = Field(min_length=1)
actor_name: str = Field(min_length=1)
figure: PictureAsset
remaining_group: FigureGroup
subtracted_group: FigureGroup
equation: SubtractionEquation
class SubtractWithImageReferenceRequest(BaseModel):
object_name: str = Field(min_length=1)
initial_quantity: PositiveInt
removed_quantity: PositiveInt
figure: PictureAsset
actor_name: str = Field(default="Diego", min_length=1)

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,36 @@
from typing import Literal
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 ItemGroup(BaseModel):
side: Literal["left", "right"]
picture: PictureAsset
quantity: PositiveInt
class MoreItemsComparison(BaseModel):
position: PositiveInt
left_group: ItemGroup
right_group: ItemGroup
answer_side: Literal["left", "right"]
has_answer_box: bool = True
class WhereAreMoreItemsProblem(BaseModel):
instructions: str = "Marca dónde hay más."
comparisons: list[MoreItemsComparison]
class WhereAreMoreItemsRequest(BaseModel):
available_pictures: list[PictureAsset] = Field(min_length=1)
comparison_count: PositiveInt = 3
min_quantity: PositiveInt = 1
max_quantity: PositiveInt = 10
seed: int | None = None