Pydantic AI leaned into the one thing Python agent frameworks were missing: type safety at the tool boundary. Define a tool as a function with Pydantic-validated inputs and outputs, and the framework handles the LLM-side schema and the runtime-side coercion. It's the right shape for production agents.
Lumbox as Pydantic AI tools
from pydantic_ai import Agent
from pydantic import BaseModel
from lumbox import Lumbox
lumbox = Lumbox(api_key=os.environ["LUMBOX_API_KEY"])
class Inbox(BaseModel):
inbox_id: str
address: str
class OtpResult(BaseModel):
otp: str
received_at: str
email_agent = Agent(
"anthropic:claude-opus-4-7",
system_prompt="You handle email verification for signup flows.",
)
@email_agent.tool_plain
def create_inbox(purpose: str) -> Inbox:
"""Create a fresh inbox for a verification task."""
inbox = lumbox.inboxes.create(display_name=purpose)
return Inbox(inbox_id=inbox.id, address=inbox.address)
@email_agent.tool_plain
def wait_for_otp(inbox_id: str, timeout: int = 120) -> OtpResult:
"""Block until an OTP arrives in the inbox."""
result = lumbox.inboxes.wait_for_otp(inbox_id, timeout=timeout)
return OtpResult(otp=result.otp, received_at=result.received_at)
Why type-safe tools matter for email
When an agent calls wait_for_otp with a timeout, you don't want a runtime error because the LLM passed "60s" instead of 60. Pydantic coerces at the tool boundary. Same for inbox IDs — Pydantic ensures you always get a well-formed inbox_id, never None.
Error handling
If the OTP wait times out, the tool raises — Pydantic AI surfaces the typed error back to the agent, which can retry or escalate. Cleaner than parsing string error messages out of generic HTTP responses. lumbox.co.