← All posts

How to Add a Web Search and Fetch Tool to a Pydantic AI Agent

Pydantic AI has become the framework of choice for Python developers who want agents that behave like the rest of their codebase: typed, validated, and easy to reason about. You get structured outputs backed by Pydantic models, dependency injection through RunContext, and tools declared with a plain decorator. What you do not get out of the box is a model that can see the web. The LLM underneath still stops at its training cutoff, so the moment a user asks about something recent, the typed, validated answer you hand back can be confidently wrong.

The fix is a pair of tools: one that searches the web and one that fetches a specific page. This post wires both into a Pydantic AI agent using @agent.tool, keeps the whole thing type-safe, and grounds the model's answers in real page content.

Why the model still needs help

A Pydantic AI agent is only as current as the model behind it. Ask "what changed in the latest release of this library" or "what is this company charging now" and the model interpolates from stale training data. It rarely tells you it is guessing. The output validates cleanly against your schema and still describes a world that no longer exists.

Retrieval solves this. Instead of asking the model to remember the web, you give it tools to read the web at request time and instruct it to answer from what it reads. In Pydantic AI, a tool is just a function you decorate, and the framework generates the JSON schema from your type hints. That is the whole appeal: the tool boundary is typed the same way everything else is.

Two tools: search and fetch

The pattern that works in production is a search tool and a fetch tool. Search finds candidate URLs and, ideally, returns the content of those pages so the model can answer in one round. Fetch pulls a single known URL when the model already has a link and needs the full text.

You could build the machinery behind these tools yourself. Grab a search API, add an HTTP client, parse HTML with a readability pass, strip navigation and cookie banners, and hand the result to the model. For a few cooperative sites this works. Then you hit pages that render with JavaScript and return an empty shell to a plain GET, sites behind Cloudflare that block datacenter IPs, and an HTML-to-markdown pipeline that breaks on every third site. None of that is your agent. It is infrastructure you now maintain forever.

link.sc collapses that into two endpoints. /v1/search takes a query and returns ranked results with full-page markdown already extracted. /v1/fetch takes a URL and returns clean markdown, JSON, HTML, or a screenshot. It handles rendering, proxies, and retries by escalating cheapest-first, so your tool function stays a few lines long.

Typed dependencies for the client

Pydantic AI wants shared resources passed in through dependencies rather than reached for as globals. That keeps tools testable and makes the API key explicit. Define a small dataclass holding an async HTTP client and the key, and declare it as the agent's deps_type.

from dataclasses import dataclass
import httpx
from pydantic_ai import Agent, RunContext


@dataclass
class Deps:
    client: httpx.AsyncClient
    api_key: str


agent = Agent(
    "anthropic:claude-opus-4-8",
    deps_type=Deps,
    system_prompt=(
        "Answer using the search and fetch tools. Do not rely on prior "
        "knowledge for anything time-sensitive. If the tools do not return "
        "the answer, say so. Cite the source URL for every claim."
    ),
)

The model identifier is a string, so swapping anthropic:claude-opus-4-8 for another provider is a one-line change and none of the tool code moves.

Wiring the tools with @agent.tool

Now the two tools. The @agent.tool decorator gives each function a RunContext[Deps] as its first argument, which is how it reaches the shared client and key. Everything after that is a normal typed function, and Pydantic AI turns the signature into the schema the model sees.

BASE = "https://api.link.sc/v1"


@agent.tool
async def web_search(ctx: RunContext[Deps], query: str) -> str:
    """Search the web for current information and return page content."""
    r = await ctx.deps.client.post(
        f"{BASE}/search",
        headers={"x-api-key": ctx.deps.api_key},
        json={"q": query},
    )
    r.raise_for_status()
    results = r.json()["results"][:3]
    return "\n\n".join(
        f"[{i + 1}] {hit['title']} ({hit['url']})\n{hit['content']}"
        for i, hit in enumerate(results)
    )


@agent.tool
async def fetch_url(ctx: RunContext[Deps], url: str) -> str:
    """Fetch a single URL and return its content as clean markdown."""
    r = await ctx.deps.client.post(
        f"{BASE}/fetch",
        headers={"x-api-key": ctx.deps.api_key},
        json={"url": url, "format": "markdown"},
    )
    r.raise_for_status()
    return r.json()["content"]

Two things are worth noticing. The docstrings are not decoration: Pydantic AI feeds them to the model as the tool descriptions, so a clear sentence about when to use each tool measurably improves how the agent chooses between them. And because web_search already returns full-page markdown, the model usually answers without a separate fetch round, which keeps both latency and token count down.

Running it

Construct the dependencies, open one HTTP client for the run, and call the agent. Pydantic AI owns the loop: it sends the message, runs any tools the model requests, feeds the results back, and repeats until the model produces a final answer.

import asyncio


async def main():
    async with httpx.AsyncClient(timeout=60) as client:
        deps = Deps(client=client, api_key="lsc_YOUR_KEY")
        result = await agent.run(
            "What changed in the latest Python release?",
            deps=deps,
        )
        print(result.output)


asyncio.run(main())

The model calls web_search, reads the returned markdown, optionally calls fetch_url on the most promising result, and answers from what it actually read rather than from memory.

Getting a typed answer back

The reason you reached for Pydantic AI in the first place was structured output, so put the web tools to work behind a schema. Declare an output_type and the agent validates the model's final answer against it, tools and all.

from pydantic import BaseModel


class Release(BaseModel):
    version: str
    headline_features: list[str]
    source_url: str


typed_agent = Agent(
    "anthropic:claude-opus-4-8",
    deps_type=Deps,
    output_type=Release,
)

Register the same two tools on typed_agent, and now result.output is a validated Release object. The model gathers live facts through the tools and the framework guarantees the shape you get back. That combination, current data plus a typed contract, is exactly what makes Pydantic AI worth using for real work.

Grounding is a prompt, not a tool

Tool access alone does not stop hallucinations. The instruction in the system prompt above does the actual work: answer from the tool results, admit when they come up empty, and cite the source URL for each claim. That shifts the model's job from recall to reading comprehension over text you handed it, which is what LLMs are genuinely good at. Requiring citations also makes wrong answers easy to catch, since you can click the link and check.

If you want the same idea in other stacks, the general version is covered in giving an AI agent internet access, and the search-read-answer pipeline is broken down in real-time web search for LLMs.

What it costs

Two meters matter: LLM tokens and web-data calls. Keep tokens down by returning markdown instead of raw HTML and by capping search results at the top three, as the code above does. On link.sc, one page or one search is roughly one credit, and there are 500 free credits a month, enough to build and test a real agent before you pay anything.

The upshot: a Pydantic AI agent with live web access is about 40 lines of typed Python. The framework handles the loop and the validation, the two decorated tools handle the retrieval, and link.sc handles the messy part of actually reading arbitrary pages.


Give your Pydantic AI agent live web access today. Grab a key at link.sc/register and start with 500 free credits a month.