Initial commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Refactored to Anthropic progressive disclosure pattern
|
||||
- Updated description with "Use PROACTIVELY when..." format
|
||||
- Removed version/author/category/tags from frontmatter
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial release of JSON Outputs Implementer skill
|
||||
- Complete workflow covering schema design, SDK integration, testing, and production
|
||||
- Pydantic and Zod integration patterns
|
||||
- Error handling for refusals, token limits, and validation failures
|
||||
- Performance optimization guidance (grammar caching, token management)
|
||||
- Contact and invoice extraction examples
|
||||
@@ -0,0 +1,90 @@
|
||||
# JSON Outputs Implementer
|
||||
|
||||
Specialized skill for implementing JSON outputs mode with guaranteed schema compliance.
|
||||
|
||||
## Purpose
|
||||
|
||||
This skill handles **end-to-end implementation** of JSON outputs mode (`output_format`), ensuring Claude's responses strictly match your JSON schema. Covers schema design, SDK integration, testing, and production deployment.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Data Extraction**: Pull structured info from text/images
|
||||
- **Classification**: Categorize content with guaranteed output format
|
||||
- **API Formatting**: Generate API-ready JSON responses
|
||||
- **Report Generation**: Create structured reports
|
||||
- **Database Operations**: Ensure type-safe inserts/updates
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Routed here by `structured-outputs-advisor`
|
||||
- Model: Claude Sonnet 4.5 or Opus 4.1
|
||||
- Beta header: `structured-outputs-2025-11-13`
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Python with Pydantic:**
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from anthropic import Anthropic
|
||||
|
||||
class Contact(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
|
||||
client = Anthropic()
|
||||
response = client.beta.messages.parse(
|
||||
model="claude-sonnet-4-5",
|
||||
betas=["structured-outputs-2025-11-13"],
|
||||
messages=[{"role": "user", "content": "Extract contact..."}],
|
||||
output_format=Contact,
|
||||
)
|
||||
|
||||
contact = response.parsed_output # Guaranteed valid
|
||||
```
|
||||
|
||||
**TypeScript with Zod:**
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const ContactSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
const response = await client.beta.messages.parse({
|
||||
model: "claude-sonnet-4-5",
|
||||
betas: ["structured-outputs-2025-11-13"],
|
||||
output_format: betaZodOutputFormat(ContactSchema),
|
||||
messages: [...]
|
||||
});
|
||||
```
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
1. **Schema Design** - Respecting JSON Schema limitations
|
||||
2. **SDK Integration** - Pydantic/Zod helpers
|
||||
3. **Error Handling** - Refusals, token limits, validation
|
||||
4. **Production Optimization** - Caching, monitoring, cost tracking
|
||||
5. **Testing** - Comprehensive validation strategies
|
||||
|
||||
## Examples
|
||||
|
||||
- [contact-extraction.py](./examples/contact-extraction.py) - Extract contact info
|
||||
- [invoice-extraction.py](./examples/invoice-extraction.py) - Complex nested schemas
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [`structured-outputs-advisor`](../structured-outputs-advisor/) - Choose the right mode
|
||||
- [`strict-tool-implementer`](../strict-tool-implementer/) - For tool validation
|
||||
|
||||
## Reference Materials
|
||||
|
||||
- [JSON Schema Limitations](../reference/json-schema-limitations.md)
|
||||
- [Best Practices](../reference/best-practices.md)
|
||||
- [API Compatibility](../reference/api-compatibility.md)
|
||||
|
||||
## Version
|
||||
|
||||
Current version: 0.1.0
|
||||
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: json-outputs-implementer
|
||||
description: >-
|
||||
Use PROACTIVELY when extracting structured data from text/images, classifying content, or formatting API responses with guaranteed schema compliance.
|
||||
Implements Anthropic's JSON outputs mode with Pydantic/Zod SDK integration.
|
||||
Covers schema design, validation, testing, and production optimization.
|
||||
Not for tool parameter validation or agentic workflows (use strict-tool-implementer instead).
|
||||
---
|
||||
|
||||
# JSON Outputs Implementer
|
||||
|
||||
## Overview
|
||||
|
||||
This skill implements Anthropic's JSON outputs mode for guaranteed schema compliance. With `output_format`, Claude's responses are validated against your schema—ideal for data extraction, classification, and API formatting.
|
||||
|
||||
**What This Skill Provides:**
|
||||
- Production-ready JSON schema design
|
||||
- SDK integration (Pydantic for Python, Zod for TypeScript)
|
||||
- Validation and error handling patterns
|
||||
- Performance optimization strategies
|
||||
- Complete implementation examples
|
||||
|
||||
**Prerequisites:**
|
||||
- Decision made via `structured-outputs-advisor`
|
||||
- Model: Claude Sonnet 4.5 or Opus 4.1
|
||||
- Beta header: `structured-outputs-2025-11-13`
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
**Use for:**
|
||||
- Extracting structured data from text/images
|
||||
- Classification tasks with guaranteed categories
|
||||
- Generating API-ready responses
|
||||
- Formatting reports with fixed structure
|
||||
- Database inserts requiring type safety
|
||||
|
||||
**NOT for:**
|
||||
- Validating tool inputs → `strict-tool-implementer`
|
||||
- Agentic workflows → `strict-tool-implementer`
|
||||
|
||||
## Response Style
|
||||
|
||||
- **Schema-first**: Design schema before implementation
|
||||
- **SDK-friendly**: Leverage Pydantic/Zod when available
|
||||
- **Production-ready**: Consider performance, caching, errors
|
||||
- **Example-driven**: Provide complete working code
|
||||
- **Limitation-aware**: Respect JSON Schema constraints
|
||||
|
||||
## Workflow
|
||||
|
||||
| Phase | Description | Details |
|
||||
|-------|-------------|---------|
|
||||
| 1 | Schema Design | → [workflow/phase-1-schema-design.md](workflow/phase-1-schema-design.md) |
|
||||
| 2 | SDK Integration | → [workflow/phase-2-sdk-integration.md](workflow/phase-2-sdk-integration.md) |
|
||||
| 3 | Error Handling | → [workflow/phase-3-error-handling.md](workflow/phase-3-error-handling.md) |
|
||||
| 4 | Testing | → [workflow/phase-4-testing.md](workflow/phase-4-testing.md) |
|
||||
| 5 | Production Optimization | → [workflow/phase-5-production.md](workflow/phase-5-production.md) |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Python Template
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from anthropic import Anthropic
|
||||
|
||||
class MySchema(BaseModel):
|
||||
field: str
|
||||
|
||||
response = client.beta.messages.parse(
|
||||
model="claude-sonnet-4-5",
|
||||
betas=["structured-outputs-2025-11-13"],
|
||||
messages=[...],
|
||||
output_format=MySchema,
|
||||
)
|
||||
result = response.parsed_output # Validated!
|
||||
```
|
||||
|
||||
### Supported Schema Features
|
||||
|
||||
✅ Basic types, enums, format strings, nested objects/arrays, required fields
|
||||
|
||||
❌ Recursive schemas, min/max constraints, string length, complex regex
|
||||
|
||||
## Reference Materials
|
||||
|
||||
- [Common Use Cases](reference/use-cases.md)
|
||||
- [Schema Limitations](reference/schema-limitations.md)
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `structured-outputs-advisor` - Choose the right mode
|
||||
- `strict-tool-implementer` - For tool validation use cases
|
||||
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Contact Information Extraction Example
|
||||
|
||||
Extracts structured contact information from unstructured text (emails, messages, etc.)
|
||||
using JSON outputs mode with Pydantic schema validation.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
from typing import Optional, List
|
||||
from anthropic import Anthropic
|
||||
import os
|
||||
|
||||
# Initialize client
|
||||
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||||
|
||||
|
||||
# Define schema with Pydantic
|
||||
class ContactInfo(BaseModel):
|
||||
"""Structured contact information extracted from text."""
|
||||
|
||||
name: str = Field(description="Full name of the contact person")
|
||||
email: EmailStr = Field(description="Email address")
|
||||
phone: Optional[str] = Field(
|
||||
None, description="Phone number in any format"
|
||||
)
|
||||
company: Optional[str] = Field(
|
||||
None, description="Company or organization name"
|
||||
)
|
||||
plan_interest: Optional[str] = Field(
|
||||
None, description="Product plan or tier they're interested in"
|
||||
)
|
||||
demo_requested: bool = Field(
|
||||
False, description="Whether they requested a product demo"
|
||||
)
|
||||
tags: List[str] = Field(
|
||||
default_factory=list,
|
||||
description="Relevant tags or categories"
|
||||
)
|
||||
|
||||
|
||||
def extract_contact(text: str) -> Optional[ContactInfo]:
|
||||
"""
|
||||
Extract contact information from unstructured text.
|
||||
|
||||
Args:
|
||||
text: Unstructured text containing contact information
|
||||
|
||||
Returns:
|
||||
ContactInfo object with extracted data, or None if request refused
|
||||
"""
|
||||
try:
|
||||
response = client.beta.messages.parse(
|
||||
model="claude-sonnet-4-5",
|
||||
max_tokens=1024,
|
||||
betas=["structured-outputs-2025-11-13"],
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": f"Extract contact information from the following text:\n\n{text}"
|
||||
}],
|
||||
output_format=ContactInfo,
|
||||
)
|
||||
|
||||
# Handle different stop reasons
|
||||
if response.stop_reason == "refusal":
|
||||
print(f"⚠️ Request refused for safety reasons")
|
||||
return None
|
||||
|
||||
if response.stop_reason == "max_tokens":
|
||||
print(f"⚠️ Response truncated - increase max_tokens")
|
||||
return None
|
||||
|
||||
# Return validated contact info
|
||||
return response.parsed_output
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error extracting contact: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
"""Run contact extraction examples."""
|
||||
|
||||
examples = [
|
||||
# Example 1: Complete contact info
|
||||
"""
|
||||
Hi, I'm John Smith from Acme Corp. You can reach me at john.smith@acme.com
|
||||
or call me at (555) 123-4567. I'm interested in your Enterprise plan and
|
||||
would love to schedule a demo next week.
|
||||
""",
|
||||
|
||||
# Example 2: Minimal info
|
||||
"""
|
||||
Contact: jane.doe@example.com
|
||||
""",
|
||||
|
||||
# Example 3: Informal message
|
||||
"""
|
||||
Hey! Bob here. Email me at bob@startup.io if you want to chat about
|
||||
the Pro plan. Thanks!
|
||||
""",
|
||||
|
||||
# Example 4: Multiple contacts (extracts first/primary)
|
||||
"""
|
||||
From: alice@company.com
|
||||
CC: support@company.com
|
||||
|
||||
Hi, I'm Alice Johnson, VP of Engineering at TechCo.
|
||||
We're evaluating your platform for our team of 50 developers.
|
||||
""",
|
||||
]
|
||||
|
||||
print("=" * 70)
|
||||
print("Contact Extraction Examples")
|
||||
print("=" * 70)
|
||||
|
||||
for i, text in enumerate(examples, 1):
|
||||
print(f"\n📧 Example {i}:")
|
||||
print(f"Input: {text.strip()[:100]}...")
|
||||
|
||||
contact = extract_contact(text)
|
||||
|
||||
if contact:
|
||||
print(f"\n✅ Extracted Contact:")
|
||||
print(f" Name: {contact.name}")
|
||||
print(f" Email: {contact.email}")
|
||||
print(f" Phone: {contact.phone or 'N/A'}")
|
||||
print(f" Company: {contact.company or 'N/A'}")
|
||||
print(f" Plan Interest: {contact.plan_interest or 'N/A'}")
|
||||
print(f" Demo Requested: {contact.demo_requested}")
|
||||
print(f" Tags: {', '.join(contact.tags) if contact.tags else 'None'}")
|
||||
else:
|
||||
print(f"\n❌ No contact extracted")
|
||||
|
||||
print("-" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Invoice Data Extraction Example
|
||||
|
||||
Extracts structured invoice data from text using JSON outputs with nested schemas.
|
||||
Demonstrates handling complex nested structures (line items, tax breakdown).
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from decimal import Decimal
|
||||
from anthropic import Anthropic
|
||||
import os
|
||||
|
||||
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||||
|
||||
|
||||
# Nested schema for line items
|
||||
class LineItem(BaseModel):
|
||||
"""Individual line item on an invoice."""
|
||||
description: str = Field(description="Item description")
|
||||
quantity: int = Field(description="Quantity ordered")
|
||||
unit_price: float = Field(description="Price per unit in USD")
|
||||
total: float = Field(description="Total for this line (quantity * unit_price)")
|
||||
|
||||
|
||||
class Invoice(BaseModel):
|
||||
"""Complete invoice structure."""
|
||||
invoice_number: str = Field(description="Invoice ID (format: INV-XXXXX)")
|
||||
date: str = Field(description="Invoice date in YYYY-MM-DD format")
|
||||
due_date: str = Field(description="Payment due date in YYYY-MM-DD format")
|
||||
customer_name: str = Field(description="Customer or company name")
|
||||
customer_email: str = Field(description="Customer email address")
|
||||
|
||||
line_items: List[LineItem] = Field(
|
||||
description="List of items on the invoice"
|
||||
)
|
||||
|
||||
subtotal: float = Field(description="Subtotal before tax in USD")
|
||||
tax_rate: float = Field(description="Tax rate as decimal (e.g., 0.08 for 8%)")
|
||||
tax_amount: float = Field(description="Tax amount in USD")
|
||||
total_amount: float = Field(description="Final total amount in USD")
|
||||
|
||||
notes: str = Field(
|
||||
default="",
|
||||
description="Additional notes or payment instructions"
|
||||
)
|
||||
|
||||
|
||||
def extract_invoice(invoice_text: str) -> Optional[Invoice]:
|
||||
"""Extract structured invoice data."""
|
||||
try:
|
||||
response = client.beta.messages.parse(
|
||||
model="claude-sonnet-4-5",
|
||||
max_tokens=2048, # Higher for complex nested structures
|
||||
betas=["structured-outputs-2025-11-13"],
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": f"Extract all invoice data from:\n\n{invoice_text}"
|
||||
}],
|
||||
output_format=Invoice,
|
||||
)
|
||||
|
||||
if response.stop_reason != "end_turn":
|
||||
print(f"⚠️ Unexpected stop reason: {response.stop_reason}")
|
||||
return None
|
||||
|
||||
return response.parsed_output
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
"""Run invoice extraction example."""
|
||||
|
||||
invoice_text = """
|
||||
INVOICE
|
||||
|
||||
Invoice Number: INV-2024-00123
|
||||
Date: 2024-01-15
|
||||
Due Date: 2024-02-15
|
||||
|
||||
Bill To:
|
||||
Acme Corporation
|
||||
John Smith, CFO
|
||||
john.smith@acme.com
|
||||
|
||||
ITEMS:
|
||||
1. Cloud Hosting - Pro Plan (x3 servers)
|
||||
Quantity: 3
|
||||
Unit Price: $299.00
|
||||
Total: $897.00
|
||||
|
||||
2. Database Storage - 500GB
|
||||
Quantity: 500
|
||||
Unit Price: $0.50
|
||||
Total: $250.00
|
||||
|
||||
3. API Calls - Premium Tier
|
||||
Quantity: 1,000,000
|
||||
Unit Price: $0.001
|
||||
Total: $1,000.00
|
||||
|
||||
4. Support - Enterprise Level
|
||||
Quantity: 1
|
||||
Unit Price: $500.00
|
||||
Total: $500.00
|
||||
|
||||
Subtotal: $2,647.00
|
||||
Tax (8.5%): $224.99
|
||||
TOTAL: $2,871.99
|
||||
|
||||
Payment Terms: Net 30
|
||||
Please remit payment to accounts@cloudprovider.com
|
||||
"""
|
||||
|
||||
print("=" * 70)
|
||||
print("Invoice Extraction Example")
|
||||
print("=" * 70)
|
||||
|
||||
invoice = extract_invoice(invoice_text)
|
||||
|
||||
if invoice:
|
||||
print(f"\n✅ Invoice Extracted Successfully\n")
|
||||
print(f"Invoice #: {invoice.invoice_number}")
|
||||
print(f"Customer: {invoice.customer_name} ({invoice.customer_email})")
|
||||
print(f"Date: {invoice.date}")
|
||||
print(f"Due: {invoice.due_date}")
|
||||
print(f"\nLine Items:")
|
||||
|
||||
for i, item in enumerate(invoice.line_items, 1):
|
||||
print(f" {i}. {item.description}")
|
||||
print(f" Qty: {item.quantity} × ${item.unit_price:.2f} = ${item.total:.2f}")
|
||||
|
||||
print(f"\nSubtotal: ${invoice.subtotal:.2f}")
|
||||
print(f"Tax ({invoice.tax_rate * 100:.1f}%): ${invoice.tax_amount:.2f}")
|
||||
print(f"TOTAL: ${invoice.total_amount:.2f}")
|
||||
|
||||
if invoice.notes:
|
||||
print(f"\nNotes: {invoice.notes}")
|
||||
|
||||
# Validation checks
|
||||
print(f"\n🔍 Validation:")
|
||||
calculated_subtotal = sum(item.total for item in invoice.line_items)
|
||||
print(f" Subtotal matches: {abs(calculated_subtotal - invoice.subtotal) < 0.01}")
|
||||
|
||||
calculated_tax = invoice.subtotal * invoice.tax_rate
|
||||
print(f" Tax calculation matches: {abs(calculated_tax - invoice.tax_amount) < 0.01}")
|
||||
|
||||
calculated_total = invoice.subtotal + invoice.tax_amount
|
||||
print(f" Total matches: {abs(calculated_total - invoice.total_amount) < 0.01}")
|
||||
|
||||
else:
|
||||
print("❌ Failed to extract invoice")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from typing import Optional
|
||||
main()
|
||||
@@ -0,0 +1,47 @@
|
||||
# JSON Schema Limitations Reference
|
||||
|
||||
## Supported Features
|
||||
|
||||
- ✅ All basic types (object, array, string, integer, number, boolean, null)
|
||||
- ✅ `enum` (primitives only)
|
||||
- ✅ `const`, `anyOf`, `allOf`
|
||||
- ✅ `$ref`, `$def`, `definitions` (local)
|
||||
- ✅ `required`, `additionalProperties: false`
|
||||
- ✅ String formats: date-time, time, date, email, uri, uuid, ipv4, ipv6
|
||||
- ✅ `minItems: 0` or `minItems: 1` for arrays
|
||||
|
||||
## NOT Supported
|
||||
|
||||
- ❌ Recursive schemas
|
||||
- ❌ Numerical constraints (minimum, maximum, multipleOf)
|
||||
- ❌ String constraints (minLength, maxLength, pattern with complex regex)
|
||||
- ❌ Array constraints (beyond minItems 0/1)
|
||||
- ❌ External `$ref`
|
||||
- ❌ Complex types in enums
|
||||
|
||||
## SDK Transformation
|
||||
|
||||
Python and TypeScript SDKs automatically remove unsupported constraints and add them to descriptions.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Schema designed with all required fields
|
||||
- [ ] JSON Schema limitations respected
|
||||
- [ ] SDK helper integrated (Pydantic/Zod)
|
||||
- [ ] Beta header included in requests
|
||||
- [ ] Error handling for refusals and token limits
|
||||
- [ ] Tested with representative examples
|
||||
- [ ] Edge cases covered (missing fields, invalid data)
|
||||
- [ ] Production optimization considered (caching, tokens)
|
||||
- [ ] Monitoring in place (latency, costs)
|
||||
- [ ] Documentation provided
|
||||
|
||||
## Important Reminders
|
||||
|
||||
1. **Use SDK helpers** - `client.beta.messages.parse()` auto-validates
|
||||
2. **Respect limitations** - No recursive schemas, no min/max constraints
|
||||
3. **Add descriptions** - Helps Claude understand what to extract
|
||||
4. **Handle refusals** - Don't retry safety refusals
|
||||
5. **Monitor performance** - Watch for cache misses and high latency
|
||||
6. **Set `additionalProperties: false`** - Required for all objects
|
||||
7. **Test thoroughly** - Edge cases often reveal schema issues
|
||||
@@ -0,0 +1,86 @@
|
||||
# Common Use Cases
|
||||
|
||||
## Use Case 1: Data Extraction
|
||||
|
||||
**Scenario**: Extract invoice data from text/images
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
class LineItem(BaseModel):
|
||||
description: str
|
||||
quantity: int
|
||||
unit_price: float
|
||||
total: float
|
||||
|
||||
class Invoice(BaseModel):
|
||||
invoice_number: str
|
||||
date: str
|
||||
customer_name: str
|
||||
line_items: List[LineItem]
|
||||
subtotal: float
|
||||
tax: float
|
||||
total_amount: float
|
||||
|
||||
response = client.beta.messages.parse(
|
||||
model="claude-sonnet-4-5",
|
||||
betas=["structured-outputs-2025-11-13"],
|
||||
max_tokens=2048,
|
||||
messages=[{"role": "user", "content": f"Extract invoice:\n{invoice_text}"}],
|
||||
output_format=Invoice,
|
||||
)
|
||||
|
||||
invoice = response.parsed_output
|
||||
# Insert into database with guaranteed types
|
||||
db.insert_invoice(invoice.model_dump())
|
||||
```
|
||||
|
||||
## Use Case 2: Classification
|
||||
|
||||
**Scenario**: Classify support tickets
|
||||
|
||||
```python
|
||||
class TicketClassification(BaseModel):
|
||||
category: str # "billing", "technical", "sales"
|
||||
priority: str # "low", "medium", "high", "critical"
|
||||
confidence: float
|
||||
requires_human: bool
|
||||
suggested_assignee: Optional[str] = None
|
||||
tags: List[str]
|
||||
|
||||
response = client.beta.messages.parse(
|
||||
model="claude-sonnet-4-5",
|
||||
betas=["structured-outputs-2025-11-13"],
|
||||
messages=[{"role": "user", "content": f"Classify:\n{ticket}"}],
|
||||
output_format=TicketClassification,
|
||||
)
|
||||
|
||||
classification = response.parsed_output
|
||||
if classification.requires_human or classification.confidence < 0.7:
|
||||
route_to_human(ticket)
|
||||
else:
|
||||
auto_assign(ticket, classification.category)
|
||||
```
|
||||
|
||||
## Use Case 3: API Response Formatting
|
||||
|
||||
**Scenario**: Generate API-ready responses
|
||||
|
||||
```python
|
||||
class APIResponse(BaseModel):
|
||||
status: str # "success" or "error"
|
||||
data: dict
|
||||
errors: Optional[List[dict]] = None
|
||||
metadata: dict
|
||||
|
||||
response = client.beta.messages.parse(
|
||||
model="claude-sonnet-4-5",
|
||||
betas=["structured-outputs-2025-11-13"],
|
||||
messages=[{"role": "user", "content": f"Process: {request}"}],
|
||||
output_format=APIResponse,
|
||||
)
|
||||
|
||||
# Directly return as JSON API response
|
||||
return jsonify(response.parsed_output.model_dump())
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
# Phase 1: Schema Design
|
||||
|
||||
**Objective**: Create a production-ready JSON schema respecting all limitations
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Define Output Structure
|
||||
|
||||
Ask the user:
|
||||
- "What fields do you need in the output?"
|
||||
- "Which fields are required vs. optional?"
|
||||
- "What are the data types for each field?"
|
||||
- "Are there nested objects or arrays?"
|
||||
|
||||
### 2. Choose Schema Approach
|
||||
|
||||
**Option A: Pydantic (Python) - Recommended**
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
class ContactInfo(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
plan_interest: Optional[str] = None
|
||||
demo_requested: bool = False
|
||||
tags: List[str] = []
|
||||
```
|
||||
|
||||
**Option B: Zod (TypeScript) - Recommended**
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const ContactInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
plan_interest: z.string().optional(),
|
||||
demo_requested: z.boolean().default(false),
|
||||
tags: z.array(z.string()).default([]),
|
||||
});
|
||||
```
|
||||
|
||||
**Option C: Raw JSON Schema**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Full name"},
|
||||
"email": {"type": "string", "description": "Email address"},
|
||||
"plan_interest": {"type": "string", "description": "Interested plan"},
|
||||
"demo_requested": {"type": "boolean"},
|
||||
"tags": {"type": "array", "items": {"type": "string"}}
|
||||
},
|
||||
"required": ["name", "email", "demo_requested"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Apply JSON Schema Limitations
|
||||
|
||||
**✅ Supported Features:**
|
||||
- All basic types: object, array, string, integer, number, boolean, null
|
||||
- `enum` (strings, numbers, bools, nulls only)
|
||||
- `const`
|
||||
- `anyOf` and `allOf` (limited)
|
||||
- `$ref`, `$def`, `definitions` (local only)
|
||||
- `required` and `additionalProperties: false`
|
||||
- String formats: date-time, time, date, email, uri, uuid, ipv4, ipv6
|
||||
- Array `minItems` (0 or 1 only)
|
||||
|
||||
**❌ NOT Supported (SDK can transform these):**
|
||||
- Recursive schemas
|
||||
- Numerical constraints (minimum, maximum)
|
||||
- String constraints (minLength, maxLength)
|
||||
- Complex array constraints
|
||||
- External `$ref`
|
||||
|
||||
### 4. Add AI-Friendly Descriptions
|
||||
|
||||
```python
|
||||
class Invoice(BaseModel):
|
||||
invoice_number: str # Field(description="Invoice ID, format: INV-XXXXX")
|
||||
date: str # Field(description="Invoice date in YYYY-MM-DD format")
|
||||
total: float # Field(description="Total amount in USD")
|
||||
items: List[LineItem] # Field(description="Line items on the invoice")
|
||||
```
|
||||
|
||||
Good descriptions help Claude understand what to extract.
|
||||
|
||||
## Output
|
||||
|
||||
Production-ready schema following Anthropic's limitations.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Phase 2: SDK Integration
|
||||
|
||||
**Objective**: Implement using SDK helpers for automatic validation
|
||||
|
||||
## Python Implementation
|
||||
|
||||
**Recommended: Use `client.beta.messages.parse()`**
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from anthropic import Anthropic
|
||||
|
||||
class ContactInfo(BaseModel):
|
||||
name: str = Field(description="Full name of the contact")
|
||||
email: str = Field(description="Email address")
|
||||
plan_interest: Optional[str] = Field(
|
||||
None, description="Plan tier they're interested in"
|
||||
)
|
||||
demo_requested: bool = Field(
|
||||
False, description="Whether they requested a demo"
|
||||
)
|
||||
|
||||
client = Anthropic()
|
||||
|
||||
def extract_contact(text: str) -> ContactInfo:
|
||||
"""Extract contact information from text."""
|
||||
response = client.beta.messages.parse(
|
||||
model="claude-sonnet-4-5",
|
||||
max_tokens=1024,
|
||||
betas=["structured-outputs-2025-11-13"],
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": f"Extract contact information from: {text}"
|
||||
}],
|
||||
output_format=ContactInfo,
|
||||
)
|
||||
|
||||
# Handle edge cases
|
||||
if response.stop_reason == "refusal":
|
||||
raise ValueError("Claude refused the request")
|
||||
|
||||
if response.stop_reason == "max_tokens":
|
||||
raise ValueError("Response truncated - increase max_tokens")
|
||||
|
||||
# Automatically validated
|
||||
return response.parsed_output
|
||||
|
||||
# Usage
|
||||
contact = extract_contact("John Smith (john@example.com) wants Enterprise plan")
|
||||
print(contact.name, contact.email) # Type-safe access
|
||||
```
|
||||
|
||||
## TypeScript Implementation
|
||||
|
||||
```typescript
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { z } from 'zod';
|
||||
import { betaZodOutputFormat } from '@anthropic-ai/sdk/helpers/beta/zod';
|
||||
|
||||
const ContactInfoSchema = z.object({
|
||||
name: z.string().describe("Full name of the contact"),
|
||||
email: z.string().email().describe("Email address"),
|
||||
plan_interest: z.string().optional().describe("Plan tier interested in"),
|
||||
demo_requested: z.boolean().default(false).describe("Demo requested"),
|
||||
});
|
||||
|
||||
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
||||
|
||||
async function extractContact(text: string) {
|
||||
const response = await client.beta.messages.parse({
|
||||
model: "claude-sonnet-4-5",
|
||||
max_tokens: 1024,
|
||||
betas: ["structured-outputs-2025-11-13"],
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: `Extract contact information from: ${text}`
|
||||
}],
|
||||
output_format: betaZodOutputFormat(ContactInfoSchema),
|
||||
});
|
||||
|
||||
if (response.stop_reason === "refusal") {
|
||||
throw new Error("Claude refused the request");
|
||||
}
|
||||
|
||||
if (response.stop_reason === "max_tokens") {
|
||||
throw new Error("Response truncated - increase max_tokens");
|
||||
}
|
||||
|
||||
return response.parsed_output;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const contact = await extractContact("John Smith (john@example.com)...");
|
||||
console.log(contact.name, contact.email); // Fully typed
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Working implementation with SDK validation.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Phase 3: Error Handling
|
||||
|
||||
**Objective**: Handle refusals, token limits, and validation errors
|
||||
|
||||
## Key Error Scenarios
|
||||
|
||||
### 1. Safety Refusals (`stop_reason: "refusal"`)
|
||||
|
||||
```python
|
||||
if response.stop_reason == "refusal":
|
||||
logger.warning(f"Request refused: {input_text}")
|
||||
# Don't retry - respect safety boundaries
|
||||
return None # or raise exception
|
||||
```
|
||||
|
||||
### 2. Token Limit Reached (`stop_reason: "max_tokens"`)
|
||||
|
||||
```python
|
||||
if response.stop_reason == "max_tokens":
|
||||
# Retry with higher limit
|
||||
return extract_with_higher_limit(text, max_tokens * 1.5)
|
||||
```
|
||||
|
||||
### 3. Schema Validation Errors (SDK raises exception)
|
||||
|
||||
```python
|
||||
from pydantic import ValidationError
|
||||
|
||||
try:
|
||||
result = response.parsed_output
|
||||
except ValidationError as e:
|
||||
logger.error(f"Schema validation failed: {e}")
|
||||
# Should be rare - indicates schema mismatch
|
||||
raise
|
||||
```
|
||||
|
||||
### 4. API Errors (400 - schema too complex)
|
||||
|
||||
```python
|
||||
from anthropic import BadRequestError
|
||||
|
||||
try:
|
||||
response = client.beta.messages.parse(...)
|
||||
except BadRequestError as e:
|
||||
if "too complex" in str(e).lower():
|
||||
# Simplify schema
|
||||
logger.error("Schema too complex, simplifying...")
|
||||
raise
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Robust error handling for production deployments.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Phase 4: Testing
|
||||
|
||||
**Objective**: Validate schema works with representative data
|
||||
|
||||
## Test Coverage
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def extractor():
|
||||
return ContactExtractor()
|
||||
|
||||
def test_complete_contact(extractor):
|
||||
"""Test with all fields present."""
|
||||
text = "John Smith (john@example.com) interested in Enterprise plan, wants demo"
|
||||
result = extractor.extract(text)
|
||||
|
||||
assert result.name == "John Smith"
|
||||
assert result.email == "john@example.com"
|
||||
assert result.plan_interest == "Enterprise"
|
||||
assert result.demo_requested is True
|
||||
|
||||
def test_minimal_contact(extractor):
|
||||
"""Test with only required fields."""
|
||||
text = "Contact: jane@example.com"
|
||||
result = extractor.extract(text)
|
||||
|
||||
assert result.email == "jane@example.com"
|
||||
assert result.name is not None # Claude should infer or extract
|
||||
assert result.plan_interest is None # Optional field
|
||||
assert result.demo_requested is False # Default
|
||||
|
||||
def test_invalid_input(extractor):
|
||||
"""Test with insufficient data."""
|
||||
text = "This has no contact information"
|
||||
# Depending on requirements, might raise or return partial data
|
||||
result = extractor.extract(text)
|
||||
# Define expected behavior
|
||||
|
||||
def test_refusal_scenario(extractor):
|
||||
"""Test that refusals are handled."""
|
||||
# Test with potentially unsafe content
|
||||
# Verify graceful handling without crash
|
||||
pass
|
||||
|
||||
def test_token_limit(extractor):
|
||||
"""Test with very long input."""
|
||||
text = "..." * 10000 # Very long text
|
||||
# Verify either succeeds or raises appropriate error
|
||||
pass
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Comprehensive test suite covering edge cases.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Phase 5: Production Optimization
|
||||
|
||||
**Objective**: Optimize for performance, cost, and reliability
|
||||
|
||||
## 1. Grammar Caching Strategy
|
||||
|
||||
The first request compiles a grammar from your schema (~extra latency). Subsequent requests use cached grammar (24-hour TTL).
|
||||
|
||||
**Cache Invalidation Triggers:**
|
||||
- Schema structure changes
|
||||
- Tool set changes (if using tools + JSON outputs together)
|
||||
- 24 hours of non-use
|
||||
|
||||
**Best Practices:**
|
||||
```python
|
||||
# ✅ Good: Finalize schema before production
|
||||
CONTACT_SCHEMA = ContactInfo # Reuse same schema
|
||||
|
||||
# ❌ Bad: Dynamic schema generation
|
||||
def get_schema(include_phone: bool): # Different schemas = cache misses
|
||||
if include_phone:
|
||||
class Contact(BaseModel):
|
||||
phone: str
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
## 2. Token Cost Management
|
||||
|
||||
Structured outputs add tokens via system prompt:
|
||||
```python
|
||||
# Monitor token usage
|
||||
response = client.beta.messages.parse(...)
|
||||
print(f"Input tokens: {response.usage.input_tokens}")
|
||||
print(f"Output tokens: {response.usage.output_tokens}")
|
||||
|
||||
# Optimize descriptions for token efficiency
|
||||
# ✅ Good: Concise but clear
|
||||
name: str = Field(description="Full name")
|
||||
|
||||
# ❌ Excessive: Too verbose
|
||||
name: str = Field(description="The complete full name of the person including first name, middle name if available, and last name")
|
||||
```
|
||||
|
||||
## 3. Monitoring
|
||||
|
||||
```python
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class StructuredOutputMetrics:
|
||||
latency_ms: float
|
||||
input_tokens: int
|
||||
output_tokens: int
|
||||
cache_hit: bool # Infer from latency
|
||||
stop_reason: str
|
||||
|
||||
def track_metrics(response, start_time) -> StructuredOutputMetrics:
|
||||
latency = (time.time() - start_time) * 1000
|
||||
|
||||
return StructuredOutputMetrics(
|
||||
latency_ms=latency,
|
||||
input_tokens=response.usage.input_tokens,
|
||||
output_tokens=response.usage.output_tokens,
|
||||
cache_hit=latency < 500, # Heuristic: fast = cache hit
|
||||
stop_reason=response.stop_reason,
|
||||
)
|
||||
|
||||
# Track in production
|
||||
metrics = track_metrics(response, start_time)
|
||||
if metrics.latency_ms > 1000:
|
||||
logger.warning(f"Slow structured output: {metrics.latency_ms}ms")
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Production-optimized implementation with caching and monitoring.
|
||||
Reference in New Issue
Block a user