Structured Output

Structured output (aka “extraction”) is the most powerful way to leverage generative AI. 💪

# This page will use the following imports:

from lasagna import Model, EventCallback, AgentRun
from lasagna import (
    extract_last_message,
    extraction,
    flat_messages,
    noop_callback,
    easy_extract,
)
from lasagna import known_models
from lasagna.tui import tui_input_loop

import os

from pydantic import BaseModel, Field

from dotenv import load_dotenv

We need to set up our “binder” (see the quickstart guide for what this is).

load_dotenv()

if os.environ.get('OPENAI_API_KEY'):
    print('Using OpenAI')
    binder = known_models.openai_gpt_5_mini_binder

elif os.environ.get('ANTHROPIC_API_KEY'):
    print('Using Anthropic')
    binder = known_models.anthropic_claude_sonnet_4_binder

else:
    assert False, "Neither OPENAI_API_KEY nor ANTHROPIC_API_KEY is set! We need at least one to do this demo."
Using OpenAI

The Power of Structured Output

Consider this…

  • The most popular agentic use-case right now is Retrieval Augmented Generation (RAG).
  • RAG is just an example of tool calling.
  • Tool calling is just an example of structured output.

Structured output is the real hero behind the agentic revolution that is to come. 🤯🤯🤯

About Grammars

Generative models that support grammar-restricted generation are the best at doing structured output. Such models guarantee that your specified output schema* will be adhered to.

* Schema != Content

While formal grammars guarantee that output schemas are followed, that should not be confused with correct output.

For example, if the schema says that age should be extracted as an integer, then you can be sure you’ll get an age as an integer. But it may not be the correct value!

Still, grammars are much better than no grammars, so you’ll want to favor generative AI models that can follow a grammar.

Structured Output in Lasagna AI

In Lasagna AI, you specify your desired output schema as a combination of Pydantic types and TypedDict types.

Here’s an example, using Pydantic types:

class LinguisticConstruction(BaseModel):
    subject: str = Field(description='the linguistic subject of the construction')
    verb: str    = Field(description='the linguistic verb of the construction')
    object: str  = Field(description='the linguistic object of the construction')

class ExtractionModel(BaseModel):
    summary: str
    constructions: list[LinguisticConstruction]
We Recommend Pydantic

Pydantic types are preferred over TypedDict, because they let you pass string descriptions to each parameter (which, in turn, go into the model’s prompt).

Then, inside your agent, you pass your desired output type to model.extract(...).

See an example agent below:

async def linguistic_extraction_agent(
    model: Model,
    event_callback: EventCallback,
    prev_runs: list[AgentRun],
) -> AgentRun:
    # Get **ONLY** the last message from the user:
    last_message = extract_last_message(prev_runs, from_tools=False, from_extraction=False)
    assert last_message['role'] == 'human'

    # Do structured output over the user's message:
    new_message, result = await model.extract(
        event_callback,
        messages = [last_message],
        extraction_type = ExtractionModel,
    )
    assert isinstance(result, ExtractionModel)

    # Wrap the new messages into an `AgentRun` result:
    return extraction('linguistic_extraction_agent', [new_message], result)
PROMPT = """
Hey diddle diddle,
The cat and the fiddle,
The cow jumped over the moon;
The little dog laughed
To see such sport,
And the dish ran away with the spoon.
""".strip()
prev_runs: list[AgentRun] = [
    flat_messages(
        'input',
        [
            {
                'role': 'human',
                'text': PROMPT,
            },
        ],
    ),
]

bound_agent = binder(linguistic_extraction_agent)

agent_run = await bound_agent(noop_callback, prev_runs)  # type: ignore[top-level-await]

assert agent_run['type'] == 'extraction'
result = agent_run['result']
assert isinstance(result, ExtractionModel)

print(type(result))
print(result.summary)

for construction in result.constructions:
    print('   ', construction)
<class '__main__.ExtractionModel'>
Extracted main clauses and simple subject-verb-object (or equivalent) constructions from the nursery rhyme 'Hey Diddle Diddle'. Identifies interjection, elliptical phrases, finite clauses, intransitive verbs, an infinitival clause, and a prepositional complement.
    subject='(interjection)' verb='hey diddle (exclamation)' object=''
    subject='the cat and the fiddle' verb='(elliptical/none)' object=''
    subject='the cow' verb='jumped' object='over the moon'
    subject='the little dog' verb='laughed' object=''
    subject='(implicit subject for infinitive)' verb='to see' object='such sport'
    subject='the dish' verb='ran away' object='with the spoon'

Easy Extraction

The steps above are designed such that you can layer agents. There’s a lot of “wrapping” and “unwrapping” that goes on.

However, that can be overkill if you only want a simple (“easy”) extraction method.

Here’s an “easy” way to do it, if you don’t care about building complex layered agents:

result = await easy_extract(binder, PROMPT, ExtractionModel)  # type: ignore[top-level-await]

assert isinstance(result, ExtractionModel)

print(type(result))
print(result.summary)

for construction in result.constructions:
    print('   ', construction)
<class '__main__.ExtractionModel'>
Extract basic subject-verb-object and notable clause constructions from the nursery rhyme 'Hey Diddle Diddle'.
    subject="(exclamation) 'Hey'" verb='diddle (vocative/nonsense)' object=''
    subject='The cat and the fiddle' verb='' object=''
    subject='The cow' verb='jumped' object='over the moon'
    subject='The little dog' verb='laughed' object=''
    subject='(implicit: the little dog)' verb='to see / see' object='such sport'
    subject='The dish' verb='ran away' object='with the spoon'

More Opining on Structured Output

Robust software often starts with sane data formats, from which logic flows naturally. Think SQL schemas. Think algebraic data types.

Structured output is similar in spirit. You begin with the output schema, and you build your prompt around that.