Evaluating a Strands Agent
Strands agents produce structured output by handing the agent a Pydantic model:
from strands import Agent
agent = Agent(model="us.anthropic.claude-sonnet-4-5-20250929-v1:0")
result = agent("Extract the invoice from this document: ...", structured_output_model=Invoice)
invoice = result.structured_output
# `invoice` is a validated Invoice instance
The question this guide answers: how accurate is that output, and do the errors matter? With stickler.evaluate the integration is a single line. You do not write a StructuredModel, pick comparators, or annotate anything.
The whole integration
import stickler
prediction = agent(prompt, structured_output_model=Invoice).structured_output # your agent, unchanged
result = stickler.evaluate(ground_truth, prediction) # <-- the entire integration
print(result.overall_score, result.f1, result.field_scores)
ground_truth is your labeled expected output (same Invoice type). Everything else is inferred from the model: which comparator each field gets, sensible thresholds, and order-independent list matching. See Ultra Quick Start for how that inference works.
End-to-end example
import datetime
from typing import List, Optional
from pydantic import BaseModel
from strands import Agent
import stickler
# 1. The response_model your agent already uses for structured output.
class LineItem(BaseModel):
sku: str
description: str
quantity: int
unit_price: float
class Invoice(BaseModel):
invoice_id: str
vendor_name: str
invoice_date: datetime.date
total_amount: float
notes: Optional[str] = None
line_items: List[LineItem] = []
# 2. Run the agent (this is normal Strands usage).
agent = Agent(model="us.anthropic.claude-sonnet-4-5-20250929-v1:0")
DOCUMENT = """
INVOICE #INV-2024-0042
Acme Corporation Date: 2024-03-15
2x Wireless Mouse (WM-100) @ $29.99
5x USB-C Cable 1m (UC-050) @ $12.99
Total: $1,247.50 Terms: Net 30
"""
prediction = agent(
f"Extract the invoice:\n{DOCUMENT}", structured_output_model=Invoice
).structured_output
# 3. Your labeled ground truth for this document.
ground_truth = Invoice(
invoice_id="INV-2024-0042",
vendor_name="Acme Corporation",
invoice_date=datetime.date(2024, 3, 15),
total_amount=1247.50,
notes="Net 30 payment terms",
line_items=[
LineItem(sku="WM-100", description="Wireless Mouse", quantity=2, unit_price=29.99),
LineItem(sku="UC-050", description="USB-C Cable 1m", quantity=5, unit_price=12.99),
],
)
# 4. Score it.
result = stickler.evaluate(ground_truth, prediction)
print(f"Overall: {result.overall_score:.3f}")
print(f"F1: {result.f1:.3f}")
for field, score in result.field_scores.items():
print(f" {field:14} {score:.3f}")
Scoring a whole evaluation set
You usually have many labeled documents, not one. Turn the agent's
response_model into a regular StructuredModel with
StructuredModel.from_pydantic() (the same inference stickler.evaluate
uses), then the standard
Bulk Evaluation update/compute pattern
applies exactly as documented:
from stickler import StructuredModel
from stickler.structured_object_evaluator.bulk_structured_model_evaluator import (
BulkStructuredModelEvaluator,
)
InvoiceEval = StructuredModel.from_pydantic(Invoice)
evaluator = BulkStructuredModelEvaluator(target_schema=InvoiceEval)
for doc, expected in labeled_dataset: # your (document, ground_truth) pairs
prediction = agent(
f"Extract the invoice:\n{doc}", structured_output_model=Invoice
).structured_output
evaluator.update(
InvoiceEval.from_json(expected.model_dump()),
InvoiceEval.from_json(prediction.model_dump()),
)
corpus = evaluator.compute()
print(f"Corpus F1: {corpus.metrics['cm_f1']:.3f} over {corpus.document_count} docs")
This accumulates true corpus-level precision/recall/F1 (confusion-matrix
counts across every document, not a mean of per-document scores), and
everything from the Bulk Evaluation guide
applies unchanged: doc_id tracking, JSONL per-document output,
checkpointing, and error accumulation. And because InvoiceEval is an
ordinary StructuredModel, you can export its inferred config
(to_stickler_config()), edit any comparator or threshold, and rebuild, no
zero-config-specific API needed. For a closer look at any single pair,
stickler.evaluate(expected, prediction) returns the per-document
EvalResult.
Defending the numbers
When someone asks "why did vendor_name score zero?", you don't guess. You
ask the single-pair result from the end-to-end example above:
result.explain()["vendor_name"]
# {'comparator': 'LevenshteinComparator', 'threshold': 0.85, 'weight': 1.0,
# 'source': 'name-token', 'score': 0.0, 'raw_similarity': 0.5625,
# 'verdict': 'raw 0.56 < threshold 0.85 -> clipped to 0.0', 'why': [...]}
"Acme Corporation" vs "Acme Corp" fell below the 0.85 similarity threshold Stickler chose for a name field. If that is too strict for your use case, graduate to a hand-authored StructuredModel where you set the comparator, threshold, and weight per field explicitly. stickler.evaluate gets you a defensible baseline in one line; the full API is there when you outgrow it.
Requirements
Strands is an optional dependency:
pip install "stickler-eval[llm]" # brings in strands-agents
stickler.evaluate itself has no dependency on Strands; it works on any Pydantic instances. You only need [llm] to run the agent that produces them.