Skip to content

Commit 29f02a1

Browse files
authored
Merge pull request #138 from AET-DevOps26/feat/optional-ingredient-unit
Make an ingredient's quantity and unit optional
2 parents 38b189e + 316403b commit 29f02a1

15 files changed

Lines changed: 222 additions & 90 deletions

File tree

api/openapi-internal.yaml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,16 +255,24 @@ components:
255255

256256
RecipeIngredient:
257257
type: object
258-
required: [quantity, unit, name]
258+
required: [name]
259259
additionalProperties: false
260+
description: >-
261+
A measured ingredient has both quantity and unit ("200 g flour"); a counted one
262+
has a quantity only ("2 eggs"); one added to taste has neither ("salt").
263+
A unit without a quantity is meaningless and is rejected by the recipe editor,
264+
though the contract tolerates it.
260265
properties:
261266
quantity:
262267
type: number
263-
minimum: 0
268+
exclusiveMinimum: 0
269+
description: Amount of the ingredient. Omitted when the ingredient is added to taste.
264270
unit:
265271
type: string
266272
minLength: 1
267-
description: Unit of measurement (e.g. g, ml, cup, tbsp)
273+
description: >-
274+
Unit of measurement (e.g. g, ml, cup, tbsp). Omitted when the ingredient is
275+
counted as whole items rather than measured.
268276
name:
269277
type: string
270278
minLength: 1

api/openapi.yaml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -597,16 +597,24 @@ components:
597597

598598
RecipeIngredient:
599599
type: object
600-
required: [quantity, unit, name]
600+
required: [name]
601601
additionalProperties: false
602+
description: >-
603+
A measured ingredient has both quantity and unit ("200 g flour"); a counted one
604+
has a quantity only ("2 eggs"); one added to taste has neither ("salt").
605+
A unit without a quantity is meaningless and is rejected by the recipe editor,
606+
though the contract tolerates it.
602607
properties:
603608
quantity:
604609
type: number
605-
minimum: 0
610+
exclusiveMinimum: 0
611+
description: Amount of the ingredient. Omitted when the ingredient is added to taste.
606612
unit:
607613
type: string
608614
minLength: 1
609-
description: Unit of measurement (e.g. g, ml, cup, tbsp)
615+
description: >-
616+
Unit of measurement (e.g. g, ml, cup, tbsp). Omitted when the ingredient is
617+
counted as whole items rather than measured.
610618
name:
611619
type: string
612620
minLength: 1

services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_ingredient.py

Lines changed: 23 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/py-help-service/client/pyproject.toml

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/py-help-service/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,10 @@ async def generate_help(
225225
if ingredients:
226226
recipe_ctx.append("\nIngredients:")
227227
for ing in ingredients:
228-
recipe_ctx.append(f"- {ing.quantity} {ing.unit} {ing.name}")
228+
# an absent quantity or unit is Unset, which would render as "UNSET"
229+
parts = [ing.quantity, ing.unit, ing.name]
230+
line = " ".join(str(p) for p in parts if not isinstance(p, Unset))
231+
recipe_ctx.append(f"- {line}")
229232

230233
instructions = getattr(request.recipe, "instructions", None)
231234
if instructions:

services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_ingredient.py

Lines changed: 23 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/py-recipe-service/client/pyproject.toml

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/py-recipe-service/main.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@
5454

5555
# autogenerated classes must be mapped to local ones that are compatible with pydantic
5656
class LocalRecipeIngredient(BaseModel):
57-
quantity: float
58-
unit: str
57+
# Optional unlike nutrients below: omitting them is sometimes the *correct* answer
58+
# ("2 eggs" has no unit, "salt" has neither), so the llm is guided by prompt instead.
59+
quantity: float | None = None
60+
unit: str | None = None
5961
name: str
6062

6163

@@ -246,8 +248,10 @@ async def generate_recipes(
246248
f"Constraint - Allergies: {allergies} (DO NOT USE THESE)\n"
247249
f"User Context: {about}\n\n"
248250
f"Write all recipe content (title, ingredients, units and instructions) in {language}. "
249-
"Strictly use standard, lowercase abbreviated unit names (e.g., use 'tbsp' instead of 'tablespoon', "
250-
"'tsp' instead of 'teaspoon', 'g' instead of 'grams', and 'ml' instead of 'milliliters').\n"
251+
f"Abbreviate units the way {language} conventionally abbreviates them, not the way English "
252+
"does (a tablespoon is 'tbsp' in English but 'EL' in German).\n"
253+
"Omit the unit for ingredients counted as whole items (2 eggs, 1 onion) rather than "
254+
"inventing one (2 pieces egg). Always give a quantity when you give a unit.\n"
251255
"Output the nutrients for the whole recipe in total, not per portion.\n"
252256
f"Keep the JSON keys in English as specified."
253257
)
@@ -268,7 +272,9 @@ async def generate_recipes(
268272
# return array of recipes as expeted from the api spec
269273
final_recipes = []
270274
for r in response.recipes:
271-
recipe_dict = r.model_dump()
275+
# exclude_none: the spec marks an absent quantity/unit by omitting the key,
276+
# and forbids additional properties — a null would be rejected as neither.
277+
recipe_dict = r.model_dump(exclude_none=True)
272278
final_recipes.append(RecipeInput.from_dict(recipe_dict))
273279

274280
return [r.to_dict() for r in final_recipes]
@@ -305,6 +311,9 @@ async def generate_nutrients(
305311
"You are an expert nutritional scientist. Calculate the macronutrients and total calories "
306312
"for the provided recipe. Output the nutrients for the whole recipe in total, not per portion. "
307313
"Evaluate ingredient quantities, units, and base portion sizes carefully. "
314+
"An ingredient with a quantity but no unit is counted as whole items ('2 eggs') - assume a "
315+
"typical size for one item. An ingredient with neither is added to taste ('salt') - assume a "
316+
"negligible amount. "
308317
"Ensure the output strictly mirrors the exact target JSON object fields."
309318
)
310319

services/py-recipe-service/tests/test_contract.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ def _stub_provider_dependencies():
4545
recipes=[
4646
LocalRecipeInput(
4747
title="Test Recipe",
48-
ingredients=[{"quantity": 1.0, "unit": "cup", "name": "Flour"}],
48+
# One of each ingredient kind: an absent quantity/unit must be marked by
49+
# omitting the key, since the contract forbids both null and "".
50+
ingredients=[
51+
{"quantity": 1.0, "unit": "cup", "name": "Flour"},
52+
{"quantity": 2.0, "name": "Eggs"},
53+
{"name": "Salt"},
54+
],
4955
instructions=["Mix.", "Bake."],
5056
portions=2.0,
5157
nutrients=nutrients,

services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeIngredient.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,20 @@ package org.openapitools.internal.model
2626
import com.squareup.moshi.Json
2727

2828
/**
29+
* A measured ingredient has both quantity and unit (\"200 g flour\"); a counted one has a quantity only (\"2 eggs\"); one added to taste has neither (\"salt\"). A unit without a quantity is meaningless and is rejected by the recipe editor, though the contract tolerates it.
2930
*
30-
*
31-
* @param quantity
32-
* @param unit Unit of measurement (e.g. g, ml, cup, tbsp)
3331
* @param name
32+
* @param quantity Amount of the ingredient. Omitted when the ingredient is added to taste.
33+
* @param unit Unit of measurement (e.g. g, ml, cup, tbsp). Omitted when the ingredient is counted as whole items rather than measured.
3434
*/
3535

3636
data class RecipeIngredient(
37-
@Json(name = "quantity")
38-
val quantity: kotlin.Double,
39-
// Unit of measurement (e.g. g, ml, cup, tbsp)
40-
@Json(name = "unit")
41-
val unit: kotlin.String,
4237
@Json(name = "name")
4338
val name: kotlin.String,
39+
// Amount of the ingredient. Omitted when the ingredient is added to taste.
40+
@Json(name = "quantity")
41+
val quantity: kotlin.Double? = null,
42+
// Unit of measurement (e.g. g, ml, cup, tbsp). Omitted when the ingredient is counted as whole items rather than measured.
43+
@Json(name = "unit")
44+
val unit: kotlin.String? = null,
4445
)

0 commit comments

Comments
 (0)