feat: Migrate OpenAI implementation to use structured outputs (#6964)

This commit is contained in:
Michael Genson
2026-01-31 11:57:05 -06:00
committed by GitHub
parent dcf410739e
commit 570d6f1433
14 changed files with 124 additions and 273 deletions

View File

@@ -1,5 +1,3 @@
from textwrap import dedent
from pydantic import Field
from ._base import OpenAIBase
@@ -8,61 +6,26 @@ from ._base import OpenAIBase
class OpenAIRecipeIngredient(OpenAIBase):
title: str | None = Field(
None,
description=dedent(
"""
The title of the section of the recipe that the ingredient is found in. Recipes may not specify
ingredient sections, in which case this should be left blank.
Only the first item in the section should have this set,
whereas subsuquent items should have their titles left blank (unless they start a new section).
"""
),
description="Ingredient section title (e.g., 'Dry Ingredients'). Only set on the first item in each section.",
)
text: str = Field(
...,
description=dedent(
"""
The text of the ingredient. This should represent the entire ingredient, such as "1 cup of flour" or
"2 cups of onions, chopped". If the ingredient is completely blank, skip it and do not add the ingredient,
since this field is required.
If the ingredient has no text, but has a title, include the title on the
next ingredient instead.
"""
),
description="The complete ingredient text, e.g., '1 cup of flour' or '2 cups of onions, chopped'.",
)
class OpenAIRecipeInstruction(OpenAIBase):
title: str | None = Field(
None,
description=dedent(
"""
The title of the section of the recipe that the instruction is found in. Recipes may not specify
instruction sections, in which case this should be left blank.
Only the first instruction in the section should have this set,
whereas subsuquent instructions should have their titles left blank (unless they start a new section).
"""
),
description="Instruction section title. Only set on the first step in each section.",
)
text: str = Field(
...,
description=dedent(
"""
The text of the instruction. This represents one step in the recipe, such as "Preheat the oven to 350",
or "Sauté the onions for 20 minutes". Sometimes steps can be longer, such as "Bring a large pot of lightly
salted water to a boil. Add ditalini pasta and cook for 8 minutes or until al dente; drain.".
Sometimes, but not always, recipes will include their number in front of the text, such as
"1.", "2.", or "Step 1", "Step 2", or "First", "Second". In the case where they are directly numbered
("1.", "2.", "Step one", "Step 1", "Step two", "Step 2", etc.), you should not include the number in
the text. However, if they use words ("First", "Second", etc.), then those should be included.
If the instruction is completely blank, skip it and do not add the instruction, since this field is
required. If the ingredient has no text, but has a title, include the title on the next
instruction instead.
"""
description=(
"One instruction step. Do not include numeric prefixes like '1.' or 'Step 1', "
"but do include word-based prefixes like 'First' or 'Second'."
),
)
@@ -70,137 +33,57 @@ class OpenAIRecipeInstruction(OpenAIBase):
class OpenAIRecipeNotes(OpenAIBase):
title: str | None = Field(
None,
description=dedent(
"""
The title of the note. Notes may not specify a title, and just have a body of text. In this case,
title should be left blank, and all content should go in the note text. If the note title is just
"note" or "info", you should ignore it and leave the title blank.
"""
),
description="Note title. Ignore generic titles like 'Note' or 'Info' and leave blank.",
)
text: str = Field(
...,
description=dedent(
"""
The text of the note. This should represent the entire note, such as "This recipe is great for
a summer picnic" or "This recipe is a family favorite". They may also include additional prep
instructions such as "to make this recipe gluten free, use gluten free flour", or "you may prepare
the dough the night before and refrigerate it until ready to bake".
If the note is completely blank, skip it and do not add the note, since this field is required.
"""
),
description="The note content, such as tips, variations, or preparation advice.",
)
class OpenAIRecipe(OpenAIBase):
name: str = Field(
...,
description=dedent(
"""
The name or title of the recipe. If you're unable to determine the name of the recipe, you should
make your best guess based upon the ingredients and instructions provided.
"""
),
description="Recipe name or title. Make your best guess if not obvious.",
)
description: str | None = Field(
...,
description=dedent(
"""
A long description of the recipe. This should be a string that describes the recipe in a few words
or sentences. If the recipe doesn't have a description, you should return None.
"""
),
None,
description="A brief description of the recipe in a few words or sentences.",
)
recipe_yield: str | None = Field(
None,
description=dedent(
"""
The yield of the recipe. For instance, if the recipe makes 12 cookies, the yield is "12 cookies".
If the recipe makes 2 servings, the yield is "2 servings". Typically yield consists of a number followed
by the word "serving" or "servings", but it can be any string that describes the yield. If the yield
isn't specified, you should return None.
"""
),
description="Recipe yield, e.g., '12 cookies' or '4 servings'.",
)
total_time: str | None = Field(
None,
description=dedent(
"""
The total time it takes to make the recipe. This should be a string that describes a duration of time,
such as "1 hour and 30 minutes", "90 minutes", or "1.5 hours". If the recipe has multiple times, choose
the longest time. If the recipe doesn't specify a total time or duration, or it specifies a prep time or
perform time but not a total time, you should return None. Do not duplicate times between total time, prep
time and perform time.
"""
),
description="Total time as text (e.g., '1 hour 30 minutes'). Use if only one time is available.",
)
prep_time: str | None = Field(
None,
description=dedent(
"""
The time it takes to prepare the recipe. This should be a string that describes a duration of time,
such as "30 minutes", "1 hour", or "1.5 hours". If the recipe has a total time, the prep time should be
less than the total time. If the recipe doesn't specify a prep time, you should return None. If the recipe
supplies only one time, it should be the total time. Do not duplicate times between total time, prep
time and coperformok time.
"""
),
description="Prep time as text, e.g., '30 minutes'. Do not duplicate total_time.",
)
perform_time: str | None = Field(
None,
description=dedent(
"""
The time it takes to cook the recipe. This should be a string that describes a duration of time,
such as "30 minutes", "1 hour", or "1.5 hours". If the recipe has a total time, the perform time should be
less than the total time. If the recipe doesn't specify a perform time, you should return None. If the
recipe specifies a cook time, active time, or other time besides total or prep, you should use that
time as the perform time. If the recipe supplies only one time, it should be the total time, and not the
perform time. Do not duplicate times between total time, prep time and perform time.
"""
),
description="Cook/perform time as text, e.g., '1 hour'. Do not duplicate total_time.",
)
ingredients: list[OpenAIRecipeIngredient] = Field(
[],
description=dedent(
"""
A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the
recipe. If the recipe has no ingredients, you should return an empty list.
Often times, but not always, ingredients are separated by line breaks. Use these as a guide to
separate ingredients.
"""
),
default_factory=list,
description="List of ingredients in order.",
)
instructions: list[OpenAIRecipeInstruction] = Field(
[],
description=dedent(
"""
A list of instructions for the recipe. Each instruction should represent one step in the recipe,
and should be inserted in the order they appear in the recipe. If the recipe has no instructions,
you should return an empty list.
Often times, but not always, instructions are separated by line breaks and/or separated by paragraphs.
Use these as a guide to separate instructions. They also may be separated by numbers or words, such as
"1.", "2.", "Step 1", "Step 2", "First", "Second", etc.
"""
),
default_factory=list,
description="List of instruction steps in order.",
)
notes: list[OpenAIRecipeNotes] = Field(
[],
description=dedent(
"""
A list of notes found in the recipe. Notes should be inserted in the order they appear in the recipe.
They may appear anywhere on the recipe, though they are typically found under the instructions.
"""
),
default_factory=list,
description="List of notes, tips, or variations.",
)