Share what a PydanticAI agent produced

PydanticAI's whole argument is that an agent's output should be a validated object rather than a hopeful string. That argument extends one step further than most people take it: if the deliverable is a report, the validated object should contain the report's address, not the report.

A tool, from a function signature

from pydantic_ai import Agent, ModelRetry
import httpx, os

agent = Agent("claude-sonnet-5", output_type=Analysis)

@agent.tool_plain
def publish_report(title: str, html: str) -> str:
    """Publish an HTML report and return its shareable URL."""
    if "<body" not in html.lower():
        raise ModelRetry("html must be a complete document with a <body>.")
    r = httpx.post(
        "https://commareports.com/api/v1/reports",
        headers={"Authorization": f"Bearer {os.environ['COMMA_API_TOKEN']}"},
        json={"title": title, "html": html},
    )
    r.raise_for_status()
    return r.json()["url"]

The ModelRetry is the part worth copying. A model that hands you a fragment instead of a document gets told why and tries again — far better than publishing a page that renders as three orphaned <div>s and finding out from the reader.

Put the URL in the type

from pydantic import BaseModel, HttpUrl

class Analysis(BaseModel):
    headline: str
    confidence: float
    report_url: HttpUrl

Now "did this run actually produce something shareable?" is answered by validation rather than by inspection. An agent that skipped the tool cannot satisfy the type, and the failure happens in your process instead of in someone's inbox.

It is also the cheap version. The document lives at the URL; your program handles a short object — the argument in context budgeting for AI reports.

Or attach the MCP server

from pydantic_ai.mcp import MCPServerStreamableHTTP

comma = MCPServerStreamableHTTP(
    "https://commareports.com/api/mcp",
    headers={"Authorization": f"Bearer {os.environ['COMMA_API_TOKEN']}"},
)
agent = Agent("claude-sonnet-5", toolsets=[comma], output_type=Analysis)

Publishing, updating and comment reading all arrive at once, from the same endpoint your editor uses — see MCP setup.

Worth knowing

  • Dependencies carry the token. If you use deps_type, put the API token there rather than reading the environment inside the tool — it makes the agent testable without a live token.
  • One report id per recurring job. Store it and update, so a nightly agent keeps one address and one comment history.
  • reports:write, plus comments:read for revisions — see scoped tokens for AI agents.
  • 5 MB per report body.

Try it

Free — unlimited reports, commenters and revisions.

Read the API reference →

Related