Grounded search & URL fetch
Give an agent current web results without running a scraping stack — provider-side grounding (Gemini Google Search) and URL fetch behind one interface.
An agent that cannot see the current web is stuck at its training cutoff, and the usual fix — bolting on a search API and a scraper — is a second stack to run and pay for. Where a provider already grounds its own answers, the better option is to use that.
visvoai-ai exposes two provider-agnostic entry points for grounded web
access (visvoai/ai/search.py), used internally by agent tools and available
directly:
from visvoai.ai import run_search, fetch_url, SearchResult, SearchSource, FetchError
result: SearchResult = run_search(
"what shipped in langgraph 0.3?",
deployment_id=None, # explicit id wins; else the SEARCH-capability default
provider=None, # scope the default resolution to one provider
system=None, # optional system instruction
api_key=None,
)
print(result.text)
for s in result.sources:
print(s.title, s.url)
print(result.queries) # the search queries the model actually fired
markdown = fetch_url("https://example.com/article")Resolution
Both functions resolve which deployment serves the request the same way:
an explicit deployment_id wins; otherwise
default_deployment(Capability.SEARCH, provider=provider) picks the
registry's curated SEARCH-capable default (currently
gemini-3-flash-preview, per DEFAULT_MODEL_FOR in model_registry.py),
optionally scoped to one provider. ValueError is raised if no deployment
serves Capability.SEARCH or the given id is unknown. The resolved
provider's .search() / .fetch_url() is then called — which raises
NotSupported if that provider's facade doesn't implement it.
What's actually returned
@dataclass
class SearchResult:
text: str
sources: List[SearchSource] = field(default_factory=list)
queries: List[str] = field(default_factory=list)
@dataclass
class SearchSource:
title: str
url: str
snippet: str = ""FetchError is raised (not returned) when a URL fetch fails, is blocked,
or returns no extractable text — it carries a user-facing reason in
.args[0] rather than a raw stack trace, so a tool built on fetch_url can
surface str(exc) directly to the model or user.
The only current implementation: Gemini
GeminiProvider.search() uses the google-genai SDK directly (not
langchain) because the grounding metadata — which web sources backed the
answer, which sub-queries were fired — is cleanest there. It configures
types.Tool(google_search=types.GoogleSearch()) at temperature=0.0 and
reads candidates[0].grounding_metadata for sources/queries.
GeminiProvider.fetch_url() uses Gemini's URL Context tool
(types.Tool(url_context=types.UrlContext())) — the page is fetched
server-side by Google, not from the caller's machine, and the model is
prompted to return it as clean, structured markdown. It scans the response's
url_context_metadata for ERROR/UNSAFE/FAILED markers and raises
FetchError if found, or if the returned text is empty (e.g. a JS-rendered
or paywalled page).
Anthropic and the OpenAI-compatible facades don't implement search() or
fetch_url() — calling either on those providers raises NotSupported.
Implementing native grounding for another provider is exactly the
extension point Providers & the model registry
describes: subclass Provider, override search/fetch_url, leave
everything else at the default.
Next: the agent loop in visvoai-core.