Files
droidrun/docs/features/structured-output.mdx

275 lines
6.3 KiB
Plaintext

---
title: 'Structured Output'
description: 'Extract structured data from device interactions using Pydantic models'
---
## Quick Start
```python
import asyncio
from pydantic import BaseModel, Field
from droidrun import DroidAgent, DroidConfig
# 1. Define output structure
class ContactInfo(BaseModel):
"""Contact information from device."""
name: str = Field(description="Full name of the contact")
phone: str = Field(description="Phone number")
email: str = Field(description="Email address", default="Not provided")
# 2. Create agent with output_model
async def main():
config = DroidConfig()
agent = DroidAgent(
goal="Find John Smith's contact information",
config=config,
output_model=ContactInfo,
)
# 3. Run and access structured output
result = await agent.run()
if result.success and result.structured_output:
contact: ContactInfo = result.structured_output
print(f"Name: {contact.name}")
print(f"Phone: {contact.phone}")
print(f"Email: {contact.email}")
asyncio.run(main())
```
---
## How It Works
### Two-Stage Process
**Stage 1: Task Execution**
- DroidAgent performs device actions while collecting required information
- System prompt is automatically injected with your Pydantic schema
- Agent completes with natural language answer containing the data
**Stage 2: Extraction (Post-Completion)**
- `StructuredOutputAgent` receives the final answer text
- Uses LLM's `astructured_predict()` to extract data into your model
- Validates against schema and returns typed object or `None`
---
## Example: Invoice Extraction
```python
from pydantic import BaseModel, Field
from typing import List
class Invoice(BaseModel):
"""Invoice information."""
invoice_number: str = Field(description="Invoice ID")
vendor_name: str = Field(description="Vendor name")
total_due: float = Field(description="Total amount in dollars")
agent = DroidAgent(
goal="Open Gmail and extract invoice from Acme Corp email",
config=DroidConfig(),
output_model=Invoice,
)
result = await agent.run()
invoice = result.structured_output
print(f"Invoice {invoice.invoice_number}: ${invoice.total_due}")
```
---
## Working with Results
### Accessing Data
```python
result = await agent.run()
if result.success:
if result.structured_output:
data = result.structured_output # Typed Pydantic object
print(f"Extracted: {data}")
else:
print(f"Extraction failed, text answer: {result.reason}")
else:
print(f"Task failed: {result.reason}")
```
### Exporting to JSON
```python
result = await agent.run()
if result.structured_output:
# Convert to JSON and save
json_str = result.structured_output.model_dump_json(indent=2)
with open("output.json", "w") as f:
f.write(json_str)
```
---
## Configuration
### Custom Extraction LLM
By default, extraction uses the `structured_output` LLM profile. If not configured, it falls back to the `fast_agent` LLM. You can specify a dedicated `structured_output` profile:
**config.yaml:**
```yaml
llm_profiles:
fast_agent:
provider: GoogleGenAI
model: gemini-3.1-flash-lite-preview
temperature: 0.3
structured_output:
provider: OpenAI
model: gpt-4o-mini
temperature: 0.0 # Low temp for consistent extraction
```
**Programmatically:**
```python
from droidrun import load_llm
config = DroidConfig()
llms = {
"fast_agent": load_llm("GoogleGenAI", "gemini-3.1-flash-lite-preview"),
"structured_output": load_llm("OpenAI", "gpt-4o-mini"),
}
agent = DroidAgent(
goal="Extract contact info for Alice",
llms=llms,
config=config,
output_model=ContactInfo,
)
```
### Reasoning Mode
Works in both direct and reasoning modes:
```python
# Direct mode
config = DroidConfig()
config.agent.reasoning = False
agent = DroidAgent(
goal="Find weather for SF",
config=config,
output_model=WeatherInfo,
)
# Reasoning mode
config.agent.reasoning = True
agent = DroidAgent(
goal="Find weather for SF",
config=config,
output_model=WeatherInfo,
)
```
---
## Best Practices
**1. Add clear field descriptions** - The LLM uses these to understand what to extract:
```python
name: str = Field(description="Full name of customer who placed order")
```
**2. Provide defaults for optional fields** - Prevents extraction failures:
```python
rating: Optional[float] = Field(description="Customer rating (1-5)", default=None)
```
**3. Guide data collection in your goal**:
```python
agent = DroidAgent(
goal="Find contact and get their phone number, email, and full name",
config=config,
output_model=ContactInfo,
)
```
---
## Troubleshooting
**Extraction returns None:**
- Verify `output_model` is passed to `DroidAgent`
- Check if task succeeded: `result.success`
- Enable debug logging: `config.logging.debug = True`
**Partial or incorrect data:**
- Add more specific field descriptions
- Mention required fields explicitly in the goal
**Validation errors:**
- Add `Optional` and defaults for uncertain fields
---
## Advanced
### Multiple Items
Extract lists of data using a model with `List` fields:
```python
class ContactList(BaseModel):
"""Multiple contacts."""
contacts: List[ContactInfo] = Field(description="List of contacts")
agent = DroidAgent(
goal="Find contacts for John Smith and Jane Doe",
config=config,
output_model=ContactList,
)
```
### Workflow Integration
Extraction happens automatically in `DroidAgent.finalize()`:
```python
@step
async def finalize(self, ctx: Context, ev: FinalizeEvent) -> ResultEvent:
result = ResultEvent(
success=ev.success,
reason=ev.reason,
steps=self.shared_state.step_number,
structured_output=None,
)
# Extract if model was provided
if self.output_model is not None and ev.reason:
structured_agent = StructuredOutputAgent(
llm=self.structured_output_llm,
pydantic_model=self.output_model,
answer_text=ev.reason,
)
extraction_result = await structured_agent.run()
if extraction_result["success"]:
result.structured_output = extraction_result["structured_output"]
return result
```
---
## Related Documentation
- [DroidAgent API](/sdk/droid-agent)
- [Pydantic Documentation](https://docs.pydantic.dev/)
- [Configuration Guide](/sdk/configuration)
- [Custom Variables](/features/custom-variables)