OpenAI Structured Outputs: Why is it important?

OpenAI Structured Outputs: Why is it important?

OpenAI just released Structured Outputs - models now reliably adhere to developer-supplied JSON Schemas.

Building reliable software often means ensuring that the data we work with follows strict formats or "schemas". Before the recent OpenAI update about structured output support, it didn’t guarantee that the data would always fit the exact structure developers needed.

On August 6, 2024, OpenAI announced support for Structured Outputs in the API. This new feature is designed to ensure that the data generated by OpenAI models perfectly matches the JSON Schemas you provide. Structured Outputs make it easier for developers to create applications that require precise and structured data, whether it’s for building smart assistants, automating data entry, or creating complex workflows.

In OpenAI tests, the latest model, gpt-4o-2024-08-06, using Structured Outputs, achieved a perfect score in following complex JSON schemas, compared to less than 40% for the older gpt-4-0613 model. This shows just how much more reliable and accurate Structured Outputs are in the latest update.

Why is Structured Outputs important?

Consistency in AI-generated outputs is crucial when integrating with different systems. When building applications that rely on AI, having data that adheres to a specific structure ensures smoother and more reliable integration. Without consistent output, developers often need to add extra layers of validation and correction, which can complicate the process and introduce potential points of failure. Structured Outputs eliminate this uncertainty by guaranteeing that the data generated by the AI models fits the exact schema you’ve defined. This makes it easier to plug AI-generated data directly into your systems without worrying about mismatches or errors.

This consistency also leads to more stable integrations. When you can trust that the data from OpenAI will always follow the same structure, you can confidently build workflows that depend on that data. Whether it’s feeding information into a database, triggering actions in another application, or performing complex multi-step processes, Structured Outputs ensure that every piece of data will be just as you expect, reducing the need for additional processing and making your entire system more efficient and reliable.

In this post, we'll make a simple example of how to use Structured Outputs. Basically, here is a high level overview of how it works.

Study Plan Generator Example

In this example, we'll make a simple Study Plan Generator sends a request to the OpenAI model gpt-4o-2024-08-06 to create a study plan. The OpenAI model returns a consistent JSON format as we specified in the response_format parameter. We pass the pydantic model StudyPlan so OpenAI will know that it needs to adhere to this format when sending back the response.

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    max_tokens=1024,
    temperature=0,
    messages=[
        {"role": "system", "content": "You are a helpful math tutor and is an expert in creating study plans for homeschooling kids."},
        {"role": "user", "content": "create a study plan to learn Algebra basics to my 14 year old"},
    ],
    response_format=StudyPlan,
)

Then the JSON data from the OpenAI response, containing all the structured information of the study plan, is then used to produce both a PDF document and an HTML file.

Key Components

  1. Pydantic Models (Lesson, Module, StudyPlan):

    • The code defines several Pydantic models to structure the data:

        class Lesson(BaseModel):
            title: str
            description: str
            duration_minutes: int
      
        class Module(BaseModel):
            title: str
            description: str
            objectives: List[str]
            lessons: List[Lesson]    
      
        class StudyPlan(BaseModel):
            name: str
            start_date: str
            end_date: str
            notes: List[str]
            modules: List[Module]
      
    • These models ensure that the AI's output conforms to specific attributes, such as title, description, and lessons for each module in the study plan. This structuring is crucial for consistent and reliable data integration.

  2. Interacting with OpenAI:

    • The code initializes an OpenAI client and sends a request to generate a study plan.

        client = OpenAI()
      
        completion = client.beta.chat.completions.parse(
            model="gpt-4o-2024-08-06",
            max_tokens=1024,
            temperature=0,
            messages=[
                {"role": "system", "content": "You are a helpful math tutor and is an expert in creating study plans for homeschooling kids."},
                {"role": "user", "content": "create a study plan to learn Algebra basics to my 14 year old"},
            ],
            response_format=StudyPlan,
        )
      
    • Here, the model is instructed to act as a math tutor and create a study plan for Algebra. The use of response_format=StudyPlan indicates that the model’s output should strictly follow the StudyPlan schema defined earlier.

  3. Parsing and Validating the Response:

    • Once the AI generates the study plan, the response is parsed and validated against the StudyPlan schema:

        event = completion.choices[0].message.parsed
      
        # Convert the Pydantic model instance to JSON (without indent)
        json_string = event.json()
      
        # Validate the JSON string against the StudyPlan schema
        try:
            study_plan_dict = json.loads(json_string)
            study_plan = StudyPlan.model_validate(study_plan_dict)
            print("The JSON string is valid and adheres to the StudyPlan schema.")
        except Exception as e:
            print("Validation error:", e)
      
    • The StudyPlan.model_validate() function checks if the JSON string conforms to the expected schema, ensuring that the output is both valid and consistent.

  4. Converting and Saving the JSON:

    • The validated study plan is then converted to a formatted JSON string and saved to a file:

        # Format JSON string with indentation using the built-in json module
        formatted_json = json.dumps(json.loads(json_string), indent=4)
      
        with open("study_plan.json", 'w') as json_file:
            json_file.write(formatted_json)
      
        print(formatted_json)
      
    • This step ensures that the AI-generated study plan is saved in a human-readable and structured format, ready for further use or sharing.

  5. Generating HTML and PDF Outputs:

    • Finally, the code converts the JSON data into HTML and PDF formats:

        generate_html(formatted_json, "study_plan.html")
        generate_pdf(formatted_json, "study_plan.pdf")
      
    • These functions likely take the structured JSON and format it into visually appealing documents, enhancing the accessibility and utility of the study plan.

Full Source Code

main.py

from pydantic import BaseModel
from typing import List, Optional
from openai import OpenAI
import json

from pydantic import BaseModel
from typing import List, Optional

from output_generators import generate_html, generate_pdf

class Lesson(BaseModel):
    title: str
    description: str
    duration_minutes: int

class Module(BaseModel):
    title: str
    description: str
    objectives: List[str]
    lessons: List[Lesson]    

class StudyPlan(BaseModel):
    name: str
    start_date: str
    end_date: str
    notes: List[str]
    modules: List[Module]

client = OpenAI()

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    max_tokens=1024,
    temperature=0,
    messages=[
        {"role": "system", "content": "You are a helpful math tutor and is an expert in creating study plans for homeschooling kids."},
        {"role": "user", "content": "create a study plan to learn Algebra basics to my 14 year old"},
    ],
    response_format=StudyPlan,
)

event = completion.choices[0].message.parsed

# Convert the Pydantic model instance to JSON (without indent)
json_string = event.json()

# Validate the JSON string against the StudyPlan schema
try:
    study_plan_dict = json.loads(json_string)
    study_plan = StudyPlan.model_validate(study_plan_dict)
    print("The JSON string is valid and adheres to the StudyPlan schema.")
except Exception as e:
    print("Validation error:", e)


# Format JSON string with indentation using the built-in json module
formatted_json = json.dumps(json.loads(json_string), indent=4)

with open("study_plan.json", 'w') as json_file:
    json_file.write(formatted_json)

print(formatted_json)

generate_html(formatted_json, "study_plan.html")
generate_pdf(formatted_json, "study_plan.pdf")

output_generators.py

from fpdf import FPDF
from typing import Dict
import json
from pathlib import Path

def generate_html(json_data: str, output_file: str) -> None:
    # Parse JSON data
    study_plan = json.loads(json_data)

    # Start building the HTML content with CSS styling
    html_content = f"""
    <html>
    <head>
        <title>{study_plan['name']}</title>
        <style>
            body {{
                font-family: 'Arial', sans-serif;
                background-color: #f4f4f4;
                margin: 0;
                padding: 20px;
                line-height: 1.6;
            }}
            .container {{
                max-width: 800px;
                margin: auto;
                background: white;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            }}
            h1 {{
                color: #333;
                font-size: 24px;
                border-bottom: 2px solid #ddd;
                padding-bottom: 10px;
            }}
            h2 {{
                color: #555;
                font-size: 20px;
                margin-top: 40px;
                border-bottom: 1px solid #ddd;
                padding-bottom: 10px;
            }}
            h3 {{
                color: #666;
                font-size: 18px;
                margin-top: 20px;
            }}
            p {{
                color: #444;
            }}
            ul {{
                list-style-type: disc;
                padding-left: 20px;
                color: #444;
            }}
            li {{
                margin-bottom: 10px;
            }}
            .module {{
                margin-bottom: 30px;
                padding: 10px;
                background-color: #f9f9f9;
            }}
            .lesson {{
                margin-left: 20px;
                padding-left: 10px;
            }}
            .label {{
                font-weight: bold;
                color: #333;
                margin-top: 10px;
            }}
        </style>
    </head>
    <body>
        <div class="container">
            <h1>Study Plan: {study_plan['name']}</h1>
            <p><strong>Start Date:</strong> {study_plan['start_date']}</p>
            <p><strong>End Date:</strong> {study_plan['end_date']}</p>
            <h2>Notes</h2>
            <ul>
    """

    for note in study_plan['notes']:
        html_content += f"<li>{note}</li>"

    html_content += "</ul><h2>Modules</h2>"

    for module in study_plan['modules']:
        html_content += f"""
        <div class="module">            
            <h3>{module['title']}</h3>
            <p>{module['description']}</p>
            <div class="label">Objectives</div>
            <ul>
        """
        for objective in module['objectives']:
            html_content += f"<li>{objective}</li>"
        html_content += "</ul> <div class='label'>Lessons</div><div class='lesson'><ul>"

        for lesson in module['lessons']:
            html_content += f"""
            <li>
                <strong>{lesson['title']}</strong><br>                
                {lesson['description']}<br>
                <div class="label">Duration:</div>
                <em>{lesson['duration_minutes']} minutes</em>
            </li>
            """

        html_content += "</ul></div></div>"

    html_content += """
        </div>
    </body>
    </html>
    """

    # Write HTML content to the specified output file
    with open(output_file, 'w') as file:
        file.write(html_content)

def generate_pdf(json_data: str, output_file: str) -> None:
    # Parse JSON data
    study_plan = json.loads(json_data)

    # Create PDF document
    pdf = FPDF()
    pdf.set_auto_page_break(auto=True, margin=15)
    pdf.add_page()

    # Add title
    pdf.set_font("Arial", 'B', 16)
    pdf.cell(0, 10, f"Study Plan: {study_plan['name']}", 0, 1, 'C')

    # Add study plan details
    pdf.set_font("Arial", '', 12)
    pdf.cell(0, 10, f"Start Date: {study_plan['start_date']}", 0, 1)
    pdf.cell(0, 10, f"End Date: {study_plan['end_date']}", 0, 1)

    # Add notes
    pdf.set_font("Arial", 'B', 14)
    pdf.cell(0, 10, "Notes:", 0, 1)
    pdf.set_font("Arial", '', 12)
    for note in study_plan['notes']:
        pdf.multi_cell(0, 10, f"- {note}")

    # Add modules with visual differentiation
    pdf.set_font("Arial", 'B', 14)
    pdf.cell(0, 10, "Modules:", 0, 1)

    for module in study_plan['modules']:
        pdf.set_font("Arial", 'B', 12)
        pdf.set_fill_color(240, 240, 240)  # Light gray background for module title
        pdf.cell(0, 10, f"Module Title: {module['title']}", 0, 1, 'L', fill=True)

        pdf.set_font("Arial", '', 12)
        pdf.multi_cell(0, 10, f"Description: {module['description']}")

        pdf.set_font("Arial", 'I', 12)
        pdf.cell(0, 10, "Objectives:", 0, 1)
        for objective in module['objectives']:
            pdf.multi_cell(0, 10, f"- {objective}")

        pdf.set_font("Arial", '', 12)
        pdf.cell(0, 10, "Lessons:", 0, 1)

        for lesson in module['lessons']:
            pdf.set_fill_color(255, 240, 240)  # Slightly colored background for lessons
            pdf.cell(0, 10, f"  Lesson Title: {lesson['title']}", 0, 1, 'L', fill=True)
            pdf.multi_cell(0, 10, f"  Description: {lesson['description']}")
            pdf.cell(0, 10, f"  Duration: {lesson['duration_minutes']} minutes", 0, 1)
            pdf.ln(5)

        pdf.ln(10)

    # Save the PDF to the specified output file
    pdf.output(output_file)

requirements.txt

annotated-types==0.7.0
anyio==4.4.0
certifi==2024.7.4
distro==1.9.0
fpdf==1.7.2
h11==0.14.0
httpcore==1.0.5
httpx==0.27.0
idna==3.7
jiter==0.5.0
openai==1.40.6
pydantic==2.8.2
pydantic_core==2.20.1
sniffio==1.3.1
tqdm==4.66.5
typing_extensions==4.12.2
💡

Running the example

Installing the dependencies using pip install then run the application.

pip install -r requirements.txt
python main.py

With Structured Outputs, we can now confidently build and integrate AI-driven solutions that are both precise and dependable, saving time and reducing the need for extensive validation or error handling. This new feature of OpenAI will make integration with different systems seamless and reliable.