Structured Output
Exercise 4 β Structured Output with Pydantic
| Phase | Difficulty | Time Estimate |
|---|---|---|
| 2 β Integration | ββ Intermediate | 20β25 min |
Learning Objectives
By the end of this exercise you will be able to:
- Define Pydantic
BaseModelschemas to describe the shape of agent output - Use the
response_formatparameter onagent.run()to request structured data - Access the parsed result via
response.valueand handle the fallback throughresponse.text
Prerequisites
| Requirement | How to verify |
|---|---|
| Exercise 3 completed | You have run Ex 3 successfully |
pydantic installed |
python3 -c "import pydantic; print(pydantic.__version__)" |
| Bing connection configured | BING_CONNECTION_ID (or equivalent) is set in repo-root .env |
| Azure CLI logged in | az account show (should return your subscription) |
Background
Why Structured Output?
When an LLM is used as a component in an automation pipeline β not just a chatbot β you need its output in a predictable, typed format. Free-form text is hard to route, store, or display in a UI.
Structured output solves this: you provide a JSON schema (derived from a Pydantic model), and the agent returns data that conforms to that schema.
How It Works in Agent Framework
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. Define a Pydantic BaseModel β
β class VenueInfoModel(BaseModel): β
β title: str | None = None β
β estimated_cost_per_person: float = 0.0 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 2. Pass the model as response_format to agent.run() β
β response = await agent.run( β
β "Find venues...", β
β response_format=VenueOptionsModel, β
β ) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 3. Access the result β
β response.value β parsed Pydantic instance (or None) β
β response.text β raw text (fallback) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
response.valueβ The SDK attempts to parse the agentβs output into your Pydantic model automatically. If successful, this is a fully typed Python object.response.textβ The raw text reply. Some backends/SDK versions return the JSON string here instead of populating.value. Always plan a fallback: if.valueisNone, tryYourModel.model_validate_json(response.text).
Tip: The fallback parsing logic is provided for you in the starter. Itβs important in production, but not the learning focus of this exercise.
Your Task
Open starter.py in this directory. You will see five TODO markers. Fill them in:
Step 1 β Define VenueInfoModel (TODO 1)
Create a Pydantic BaseModel that describes a single venue:
| Field | Type | Default |
|---|---|---|
title |
str \| None |
None |
description |
str \| None |
None |
services |
str \| None |
None |
address |
str \| None |
None |
estimated_cost_per_person |
float |
0.0 |
class VenueInfoModel(BaseModel):
"""Information about a venue."""
title: str | None = None
# ... add the remaining fields
Step 2 β Define VenueOptionsModel (TODO 2)
Create a second model that wraps a list of VenueInfoModel:
class VenueOptionsModel(BaseModel):
"""Options for a venue."""
options: list[VenueInfoModel]
Step 3 β Create the Agent with Web Search (TODO 3)
Reuse the Bing configuration pattern from Exercise 2. Create an agent with client.get_web_search_tool:
client = FoundryChatClient(project_endpoint=..., model=..., credential=cred) # NOT an async context manager in 1.2.2
async with client.as_agent(
name="venue_specialist",
instructions="...",
tools=[client.get_web_search_tool(additional_properties={...})],
) as agent:
- Pass
bing_props(already computed for you) inadditional_properties. - Include
"user_location"as well if youβd like location-aware results.
Step 4 β Call agent.run() with response_format (TODO 4)
This is the key new concept β pass your Pydantic model class to response_format:
response = await agent.run(
"Find venue options for a corporate holiday party for 50 people on December 6th, 2026 in Seattle",
response_format=VenueOptionsModel,
)
Important:
response_formatis a keyword argument toagent.run(), not to.as_agent().
Step 5 β Extract and Display Structured Data (TODO 5)
Access the parsed result and print each venueβs fields:
venue_options = response.value
if venue_options:
for option in venue_options.options:
print(f"Title: {option.title}")
# ... print the remaining fields
Hints
Work through the hints progressively β try on your own first!
π‘ Hint 1 β Field types and defaults
Use `str | None = None` for optional string fields and `float = 0.0` for numeric defaults: ```python class VenueInfoModel(BaseModel): title: str | None = None description: str | None = None services: str | None = None address: str | None = None estimated_cost_per_person: float = 0.0 ```π‘ Hint 2 β Where does response_format go?
`response_format` is a keyword argument to `agent.run()`, **not** to `as_agent()`: ```python # β Correct response = await agent.run("...", response_format=VenueOptionsModel) # β Wrong β as_agent() does not accept response_format client.as_agent(name=..., response_format=VenueOptionsModel) ```π‘ Hint 3 β Near-complete solution
```python class VenueInfoModel(BaseModel): """Information about a venue.""" title: str | None = None description: str | None = None services: str | None = None address: str | None = None estimated_cost_per_person: float = 0.0 class VenueOptionsModel(BaseModel): """Options for a venue.""" options: list[VenueInfoModel] # Inside main(): client = FoundryChatClient(project_endpoint=..., model=..., credential=cred) # NOT an async context manager in 1.2.2 async with client.as_agent( name="venue_specialist", instructions=( "You are the Venue Specialist, an expert in venue research and recommendation. " "Use web search to find venue options and return only structured data that matches the provided schema." ), tools=[ client.get_web_search_tool( additional_properties={ "user_location": {"city": "Seattle", "country": "US"}, **bing_props, } ) ], ) as agent: response = await agent.run( "Find venue options for a corporate holiday party for 50 people on December 6th, 2026 in Seattle", response_format=VenueOptionsModel, ) venue_options = response.value if venue_options: for option in venue_options.options: print(f"Title: {option.title}") print(f"Address: {option.address}") print(f"Description: {option.description}") print(f"Services: {option.services}") print(f"Cost per person: {option.estimated_cost_per_person}") print() ```Validate Your Work
1. Run the check script (offline β no Azure needed)
bash workshop/exercises/ex4_structured_output/check.sh
This verifies syntax, required code patterns, and that all TODOs are resolved.
2. Run against Azure
python3 -u workshop/exercises/ex4_structured_output/starter.py
Expected behaviour:
- The script connects to your Foundry project and creates a
venue_specialistagent. - The agent searches the web (via Bing) for venue options.
- The response is parsed into
VenueOptionsModelβ a list ofVenueInfoModelinstances. - You see structured output like:
Title: The Ballroom at ...
Address: 123 Main St, Seattle, WA
Description: An elegant event space ...
Services: Full catering, AV equipment ...
Cost per person: 85.0
- If
response.valueisNone, the fallback logic parsesresponse.textas JSON and still produces structured output.
Bonus Challenges
- Design your own model β Create a Pydantic
BaseModelfor a different domain (e.g.,RecipeModelwith ingredients and steps, orFlightOptionModelwith airline, price, and duration). Use it asresponse_format. - Add field validators β Use Pydanticβs
@field_validatorto enforce constraints (e.g.,estimated_cost_per_personmust be β₯ 0). - Nested models β Try adding a nested
AddressModelinsideVenueInfoModelwith structured street/city/state fields.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
response.value is None |
Backend returned JSON in .text instead |
The provided fallback logic handles this β check the console for β(parsed from response.text)β |
ValidationError from Pydantic |
Schema fields donβt match agent output | Ensure your field names and types match the model definitions exactly |
RuntimeError: Hosted web search requires a Bing connection |
BING_CONNECTION_ID not set |
Set it in repo-root .env β see Exercise 2 for details |
Failed to resolve model info |
Deployment name mismatch | Check FOUNDRY_MODEL in .env matches your Foundry project |
Cannot resolve ... host via DNS |
Private networking | Use a public endpoint or run from the correct network |
ModuleNotFoundError: pydantic |
pydantic not installed | Run pip install -r requirements.txt |
Solution Reference
See the complete working solution at: src/demo4_structured_output.py