PydanticAI + Anchor: Browser Agents in Python

Hands On
Jun 30
by Idan Raman
PydanticAI + Anchor: Browser Agents in Python

Most agent frameworks treat output as a best-effort string. PydanticAI does not. Built by the Pydantic team, it enforces typed tool parameters and structured output schemas at runtime—so your agent either returns a valid ResearchResult or retries, never a half-formed blob of text.

Pair that with Anchor's managed cloud browser infrastructure—isolated sessions, built-in anti-detect, residential IPs, native CAPTCHA solving—and you get a stack where the browser layer is handled for you and your agent logic stays clean and testable.

What We're Building

A research agent that accepts a plain-English query, opens an Anchor browser session, navigates relevant pages, and returns a typed ResearchResult with a summary, source URLs, and a confidence score. The LLM can't hallucinate a field that doesn't exist in your schema—PydanticAI enforces it.

Setup

pip install pydantic-ai anchor-browser playwright
playwright install chromium
export ANCHOR_API_KEY=your_anchor_key
export ANTHROPIC_API_KEY=your_anthropic_key

Imports and Client Initialization

import os
import asyncio
from dataclasses import dataclass
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from anchor_browser import AnchorClient
from playwright.async_api import async_playwright

Define Dependencies and Output Schema

PydanticAI's dependency injection threads context through every tool call without globals. The output schema is a plain Pydantic model—nothing framework-specific.

@dataclass
class BrowserDeps:
    anchor: AnchorClient
    cdp_url: str

class ResearchResult(BaseModel):
    summary:    str        = Field(description="Concise answer to the research question")
    sources:    list[str]  = Field(description="URLs consulted during research")
    confidence: float      = Field(ge=0.0, le=1.0, description="Confidence score 0–1")

Define the Browser Tool

The @agent.tool decorator registers the function as an LLM-callable tool. The docstring becomes the description the model sees—no separate schema definition needed.

agent = Agent(
    "anthropic:claude-sonnet-4-6",
    deps_type=BrowserDeps,
    output_type=ResearchResult,
    instructions=(
        "You are a research assistant with access to a live web browser. "
        "Navigate relevant pages, extract facts, and return a structured summary."
    ),
)

@agent.tool
async def browse(ctx: RunContext[BrowserDeps], url: str) -> str:
    # Navigate to a URL; returns title + body text (first 4000 chars).
    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(ctx.deps.cdp_url)
        context = browser.contexts[0]
        page    = context.pages[0] if context.pages else await context.new_page()
        await page.goto(url, wait_until="domcontentloaded")
        title = await page.title()
        text  = await page.inner_text("body")
        await browser.close()
    return f"Title: {title}\n\n{text[:4000]}"

Run It

async def main(query: str) -> ResearchResult:
    anchor  = AnchorClient(api_key=os.environ["ANCHOR_API_KEY"])
    session = anchor.sessions.create(
        proxy_country="us",
        options={"adblock": True},
    )

    deps   = BrowserDeps(anchor=anchor, cdp_url=session.data.cdp_url)
    result = await agent.run(query, deps=deps)

    anchor.sessions.delete(session.data.id)
    return result.output

if __name__ == "__main__":
    result = asyncio.run(
        main("What are the key features of Anchor Browser's VPN for AI agents?")
    )
    print(result.summary)
    print("Sources:", result.sources)
    print(f"Confidence: {result.confidence:.0%}")

PydanticAI automatically retries the final structured-output step if the model returns something that doesn't match ResearchResult. You never need to write a JSON parser or a try/except around the response.

Why PydanticAI + Anchor Works Well Together

Structured output, always. PydanticAI validates LLM output against your schema and retries on failures. You get a ResearchResult object with guaranteed types—not a string to parse and sanitize.

Dependency injection done right. The BrowserDeps dataclass holds your Anchor session and gets threaded through every tool call via RunContext. No module-level state, no threading issues.

Model-agnostic by design. Swap anthropic:claude-sonnet-4-6 for openai:gpt-4o or google:gemini-2.5-flash without touching your tools or output schema. Test against a cheap model, deploy on a capable one.

Session isolation. Each agent.run() gets its own Anchor session—no cookie bleed, no shared fingerprint state between concurrent runs. Anchor's per-session residential IPs mean your agent looks like a real human on every request.

Production Tips

Session pooling. For high-throughput pipelines, pre-warm a pool of Anchor sessions at startup and check them out per run. Cold-starting a browser per query adds latency you don't need.

Retry budgets. PydanticAI retries tool calls on validation failures. Cap them with max_retries=2 on your Agent to prevent runaway costs when a target page returns garbage.

Authentication flows. Pass credentials via BrowserDeps and use Anchor's OmniConnect for sites that require OAuth or 2FA. The authenticated session state persists across tool calls.

Geo-routing. Anchor exposes proxy_country at session creation time. Spin up region-specific sessions to scrape country-gated pricing pages or localized content without managing proxy infrastructure yourself.

What's Next

  • Add a search tool alongside browse so the agent discovers URLs before it visits them
  • Use PydanticAI's streaming API (agent.run_stream()) to surface partial results as the agent browses
  • Combine with Anchor Replicate to replay human-recorded workflows as structured agent subtasks

Get Started

PydanticAI docs  ·  Anchor documentation  ·  Start your free Anchor trial →

Stay ahead in browser automation

We respect your inbox. Privacy policy

Welcome aboard! Thanks for signing up
Oops! Something went wrong while submitting the form.