Defining structured outputs
Use Pydantic models to define the output schema:from polos import Agent
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class PersonName(BaseModel):
first: str = Field(description="First name")
last: str = Field(description="Last name")
class Gender(str, Enum):
MALE = "male"
FEMALE = "female"
UNKNOWN = "unknown"
class Person(BaseModel):
"""Structured person information extracted from text."""
name: PersonName = Field(description="Person's name with first and last name")
age: int = Field(description="Age in years", ge=0, le=130)
email: Optional[str] = Field(description="Email address", default=None)
location: str = Field(description="City or location", default="")
gender: Gender = Field(description="Gender of the person", default=Gender.UNKNOWN)
person_extractor = Agent(
id="person-extractor",
provider="openai",
model="gpt-4o",
system_prompt="Extract information about the person mentioned in the text"
)
import { defineAgent } from "@polos/sdk";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const personNameSchema = z.object({
first: z.string().describe("First name"),
last: z.string().describe("Last name"),
});
const genderEnum = z.enum(["male", "female", "unknown"]);
const personSchema = z
.object({
name: personNameSchema.describe("Person's name with first and last name"),
age: z.number().int().min(0).max(130).describe("Age in years"),
email: z.string().nullable().default(null).describe("Email address"),
location: z.string().default("").describe("City or location"),
gender: genderEnum.default("unknown").describe("Gender of the person"),
})
.describe("Structured person information extracted from text.");
const personExtractor = defineAgent({
id: "person-extractor",
model: openai("gpt-4o"),
systemPrompt:
"Extract information about the person mentioned in the text",
outputSchema: personSchema,
});
Using with agent.run()
Theresult field contains a fully validated Pydantic model instance:
import asyncio
from polos import PolosClient
async def main():
client = PolosClient()
response = await person_extractor.run(
client,
"Hi, I'm Alice Johnson, 28 years old, living in San Francisco. Email: alice@example.com"
)
person = response.result # Person (Pydantic model instance)
print("Structured Output:")
print(f" Name: {person.name.first} {person.name.last}")
print(f" Age: {person.age}")
print(f" Email: {person.email or '(not provided)'}")
print(f" Location: {person.location or '(not provided)'}")
print(f" Gender: {person.gender.value}")
if __name__ == "__main__":
asyncio.run(main())
import { PolosClient } from "@polos/sdk";
async function main() {
const client = PolosClient.fromEnv();
const response = await personExtractor.run(
client,
"Hi, I'm Alice Johnson, 28 years old, living in San Francisco. Email: alice@example.com"
);
const person = response.result; // Typed via Zod inference
console.log("Structured Output:");
console.log(` Name: ${person.name.first} ${person.name.last}`);
console.log(` Age: ${person.age}`);
console.log(` Email: ${person.email ?? "(not provided)"}`);
console.log(` Location: ${person.location || "(not provided)"}`);
console.log(` Gender: ${person.gender}`);
}
main();
Structured Output:
Name: Alice Johnson
Age: 28
Email: alice@example.com
Location: San Francisco
Gender: unknown
Using with agent.stream()
When streaming, the output is JSON that you can parse into your Pydantic model:async def main():
client = PolosClient()
stream = await person_extractor.stream(
client,
"My name is Bob Smith, I'm 35, and I live in Austin, Texas."
)
print("Streaming structured output:")
output = ""
async for chunk in stream.text_chunks:
output += chunk
print(chunk, end="", flush=True)
print("\n\nParsed structure:")
person = Person.model_validate_json(output)
print(f" Name: {person.name.first} {person.name.last}")
print(f" Age: {person.age}")
print(f" Location: {person.location}")
if __name__ == "__main__":
asyncio.run(main())
async function main() {
const client = PolosClient.fromEnv();
const stream = await personExtractor.stream(
client,
"My name is Bob Smith, I'm 35, and I live in Austin, Texas."
);
console.log("Streaming structured output:");
let output = "";
for await (const chunk of stream.textChunks) {
output += chunk;
process.stdout.write(chunk);
}
console.log("\n\nParsed structure:");
const person = personSchema.parse(JSON.parse(output));
console.log(` Name: ${person.name.first} ${person.name.last}`);
console.log(` Age: ${person.age}`);
console.log(` Location: ${person.location}`);
}
main();
Streaming structured output:
{"name": {"first": "Bob", "last": "Smith"}, "age": 35, "email": null, "location": "Austin, Texas", "gender": "unknown"}
Parsed structure:
Name: Bob Smith
Age: 35
Location: Austin, Texas
Complex nested structures
Structured outputs support nested models, lists, and enums:from typing import List
from pydantic import BaseModel, Field
from polos import Agent, PolosClient
class Address(BaseModel):
street: str = Field(description="Street address", default="")
city: str = Field(description="City", default="")
country: str = Field(description="Country", default="")
postal_code: Optional[str] = Field(description="Postal code", default=None)
class PhoneNumber(BaseModel):
type: str = Field(description="Type of phone number (can be mobile, work, home)", default="")
number: str = Field(description="Phone number", default="")
class Contact(BaseModel):
name: str
email: str
phones: List[PhoneNumber] = []
address: Optional[Address] = None
notes: str = ""
contact_extractor = Agent(
id="contact-extractor",
provider="anthropic",
model="claude-sonnet-4-5",
system_prompt="Extract contact information from text.",
output_schema=Contact
)
client = PolosClient()
response = await contact_extractor.run(
client,
"""
John Doe
Email: john@example.com
Mobile: +1-555-0123
Work: +1-555-0199
Address: 123 Main St, New York, NY 10001
"""
)
contact = response.result
print(f"Name: {contact.name}")
for phone in contact.phones:
print(f" {phone.type}: {phone.number}")
print(f"Address: {contact.address}")
import { defineAgent, PolosClient } from "@polos/sdk";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
const addressSchema = z.object({
street: z.string().default("").describe("Street address"),
city: z.string().default("").describe("City"),
country: z.string().default("").describe("Country"),
postalCode: z.string().nullable().default(null).describe("Postal code"),
});
const phoneNumberSchema = z.object({
type: z
.string()
.default("")
.describe("Type of phone number (can be mobile, work, home)"),
number: z.string().default("").describe("Phone number"),
});
const contactSchema = z.object({
name: z.string(),
email: z.string(),
phones: z.array(phoneNumberSchema).default([]),
address: addressSchema.nullable().default(null),
notes: z.string().default(""),
});
const contactExtractor = defineAgent({
id: "contact-extractor",
model: anthropic("claude-sonnet-4-5"),
systemPrompt: "Extract contact information from text.",
outputSchema: contactSchema,
});
const client = PolosClient.fromEnv();
const response = await contactExtractor.run(
client,
`
John Doe
Email: john@example.com
Mobile: +1-555-0123
Work: +1-555-0199
Address: 123 Main St, New York, NY 10001
`
);
const contact = response.result;
console.log(`Name: ${contact.name}`);
for (const phone of contact.phones) {
console.log(` ${phone.type}: ${phone.number}`);
}
console.log(`Address: ${JSON.stringify(contact.address)}`);
Provider compatibility
Not all model providers support structured output.- Some models don’t support it at all
- Some models support structured outputs only when no tools are present
Fallback behavior
When a model doesn’t natively support structured outputs, Polos automatically:- Generates the natural language response
- Makes an additional LLM call to structure the output according to your schema
- Returns the validated Pydantic model
# This works even if the model doesn't natively support structured outputs
# Polos will use an extra LLM call if needed
response = await person_extractor.run("Extract info from: John, 30, NYC")
person = response.result # Always a validated Person instance
// This works even if the model doesn't natively support structured outputs
// Polos will use an extra LLM call if needed
const response = await personExtractor.run("Extract info from: John, 30, NYC");
const person = response.result; // Always a validated person object
Use cases
Data extraction
# Extract invoice details from text/images
invoice_extractor = Agent(
output_schema=Invoice,
system_prompt="Extract invoice data including line items, totals, dates"
)
// Extract invoice details from text/images
const invoiceExtractor = defineAgent({
outputSchema: invoiceSchema,
systemPrompt: "Extract invoice data including line items, totals, dates",
});
Form processing
# Convert natural language to structured form data
form_filler = Agent(
output_schema=ApplicationForm,
system_prompt="Fill out application forms from user descriptions"
)
// Convert natural language to structured form data
const formFiller = defineAgent({
outputSchema: applicationFormSchema,
systemPrompt: "Fill out application forms from user descriptions",
});
API responses
# Build type-safe APIs with guaranteed response structure
api_handler = Agent(
output_schema=APIResponse,
system_prompt="Process requests and return structured API responses"
)
// Build type-safe APIs with guaranteed response structure
const apiHandler = defineAgent({
outputSchema: apiResponseSchema,
systemPrompt: "Process requests and return structured API responses",
});
Content categorization
# Classify and tag content
classifier = Agent(
output_schema=ContentClassification,
system_prompt="Categorize content with tags, sentiment, topics"
)
// Classify and tag content
const classifier = defineAgent({
outputSchema: contentClassificationSchema,
systemPrompt: "Categorize content with tags, sentiment, topics",
});