Skip to main content

wesearch🕸️

PyPI version CI Python 3.12+ License: Apache-2.0 Discord

A wascally web research toolkit for agents; web search, web fetch, and publication research.

Quick Start

# Mac:
#   # Required for quick install.
#   brew install uv

# Ubuntu/Debian:
#   # Required for quick install.
#   sudo apt-get install -y curl
#   curl -LsSf https://astral.sh/uv/install.sh | sh

uv add wesearch

# Alternatively: python -m pip install wesearch

wesearch is a synchronous, batteries-included library for programmatic web access: run a search, fetch a page through a real-browser fingerprint, extract content, and look up scholarly papers across multiple providers. It is the web layer factored out of a larger agent stack, so it is built to survive bot-detection, rate limits, and flaky endpoints without a running browser in the common case.

Example

from wesearch.search.search import search
from wesearch.fetch import RequestParams, RetryParams, fetch
from wesearch.scrape import get_element_content

# Web search: walks DEFAULT_SEARCH_CHAIN; first non-empty answer wins
hits = search("denoising recursion models", num_results=10)
for r in hits:
    print(r.title, r.url)

# Fetch + scrape
body = fetch(
    "https://example.com", request=RequestParams(retry=RetryParams(timeout_sec=10))
)
title = get_element_content(body.decode("utf-8"), "h1")

# Scholarly papers: Semantic Scholar + OpenAlex, reciprocal-rank-fused by default
from wesearch.paper.search import search as paper_search
from wesearch.paper.ids import normalize_id
from wesearch.paper.details import metadata

result = paper_search("attention is all you need", limit=5)
for rec in result.records:
    print(rec.title, rec.year)

meta = metadata(*normalize_id("arXiv:1706.03762"))

Each name is imported from the submodule that defines it; the top-level __init__ re-exports nothing.

What's inside

wesearch/
├── types/           the vocabulary every layer shares; imports nothing internal
│   ├── params.py    RequestParams + Content/Retry/Observe/Policy; Transport,
│   │                  Extractor
│   └── errors.py    FetchError, BotDetectionError + subclasses
├── fetch/           the sole HTTP egress
│   ├── fetch.py     fetch(url, request=RequestParams(...)) -> body
│   ├── gate.py      host-wide per-site pacing and block recovery
│   ├── transport/   how bytes are retrieved
│   │   ├── curl.py      curl-cffi transport (TLS/JA3 browser impersonation)
│   │   ├── stdlib.py    dependency-free urllib transport
│   │   ├── zendriver.py opt-in real-Chrome backend for JS-gated pages
│   │   └── transport_routing.py  per-domain transport selection
│   ├── html_to_text.py how a fetched page becomes text
│   ├── providers/   per-site fetch strategies (reddit, google_news, x, ...)
│   └── challenge.py bot-challenge detection and classification
├── search/          web search over pluggable backends
│   └── search.py    search(...) over SearXNG / DuckDuckGo / Google;
│                      SearchResult / PaperResult / ImageResult
├── paper/           scholarly-paper lookup
│   ├── search.py    search(...) across Semantic Scholar, OpenAlex, SearXNG
│   ├── details.py   metadata / references / citations
│   ├── authors.py   author search and publication lists
│   ├── fetch.py     PDF download cascade
│   └── providers/   per-source backends (openalex, s2, searxng)
├── mcp/             the MCP surface; the only place the mcp SDK is imported
│   └── server.py    wesearch-mcp, one tool per public function
├── chrome/          real-browser fingerprints
│   ├── headers.py   Chrome request headers (incl. x-browser-validation)
│   └── useragents.py  vendored, refreshable User-Agent pools
├── web.py           fetch_web(...): fetch + provider dispatch + extraction
├── profile.py       cross-process per-(egress_ip, domain) cookie jar
└── scrape.py        get_element_content(html, selector)

Transports

fetch picks a transport per domain:

  • curl-cffi (default): TLS/JA3 browser impersonation, no browser process.
  • stdlib: dependency-free urllib fallback.
  • zendriver: a headless Chrome for JavaScript-gated pages, used only for domains that require it. One Chrome serves every process on the host: the first fetch launches it detached, later fetches attach over DevTools and work in their own tab.

A persistent per-(egress_ip, domain) cookie profile is loaded and saved transparently. Every fetch is paced host-wide per site and IP (fetch/gate.py holds the per-site table). A block locks the site briefly; one caller then probes it, a success unlocks every caller, and a failure doubles the lock. Run zendriver to clear blocks by hand: it opens a tab per challenge-locked site, and each site you load there is unlocked. Search results are cached host-wide for an hour, and a domain learned to need the browser is re-probed after a week.

Check backend health with python -m wesearch.tools (add --processes 12 to load-test the shared browser).

API keys & configuration

Everything works keyless out of the box; the environment variables below raise your rate limits or unlock a backend, and are all optional unless noted.

  • SEMANTIC_SCHOLAR_API_KEY -- optional. Without it, Semantic Scholar lookups (paper.search, paper.details) share a low-rate public tier, and paper.search(..., source="fused") may return complete=False when S2 throttles the request. Set it for a higher-throughput tier.
  • OPENALEX_EMAIL -- optional. Identifies you to OpenAlex's "polite pool" for more headroom than anonymous requests.
  • OPENALEX_API_KEY -- optional. A higher OpenAlex request budget.
  • BRAVE_API_KEY -- optional. Enables the Brave Search API, last in the default search chain because it bills per request.
  • SEARXNG_URL -- optional. The base URL of a SearXNG instance you control or trust; enables the "searxng" backend and the category tabs.

State (the cookie profile jar, cross-process rate-limit lockfiles, the browser transport's persistent Chrome profile) is written under the OS's standard per-user data directory -- XDG_DATA_HOME (or ~/.local/share) on Linux, ~/Library/Application Support on macOS, %LOCALAPPDATA% on Windows -- namespaced per component (see wesearch/lib/userdirs.py). No configuration file is required or read.

The "zendriver" transport needs a non-snap Chrome or Chromium (e.g. Google Chrome's .deb on x86_64). Snap-packaged Chromium fails every launch with BrowserUnavailableError for two reasons, neither of which the library can work around:

  1. The snap wrapper takes several seconds to expose DevTools -- far beyond the launch budget (0.5s per attempt, 6 attempts), which is sized so the curl-then-zendriver cascade fails fast on hosts with no usable browser rather than stalling every fetch.
  2. Snap's AppArmor confinement silently blocks writes under hidden home paths like ~/.local/share, so Chrome dies on its profile lock wherever the profile jar lands by default.

Development

uv sync --all-groups

Tests are tiered with pytest markers; the default run (uv run pytest) executes only the fast unit tier:

addopts = -m 'not ci_smoke and not cuda and not integration and not performance and not cluster and not slow'
  • ci_smoke -- slower package smoke tests, run explicitly in CI.
  • cuda -- requires a real CUDA device.
  • integration -- requires networking or external CLIs.
  • performance -- timing-sensitive.
  • cluster -- requires live cluster access.
  • slow -- expensive local correctness tests (JIT, full fixtures, git, bash, a fresh interpreter).
  • real_llm -- spawns a live LLM CLI; skipped unless RUN_REAL_LLM=1.

Run a specific tier with uv run pytest -m integration, or everything with uv run pytest -m ''.

The zendriver backend tests need a Chrome or Chromium binary on PATH; without one they are skipped automatically. No other system dependency is required to run the fast unit tier.

MCP server

The toolkit is directly callable by coding agents over MCP:

pip install 'wesearch[mcp]'
wesearch-mcp                  # serves stdio; register it with your MCP client

For Claude Code: claude mcp add --scope user wesearch -- wesearch-mcp.

Tools exposed: paper_search (fused Semantic Scholar + OpenAlex), paper_details, paper_references, paper_citations, paper_pdf (downloads into the user cache and returns the path), author_search, author_papers, web_search, and web_fetch (extracted page text; the default transport escalates to a headless browser when a site bot-blocks the header transports). Outputs are deliberately compact for model consumption; abstracts are truncated and empty fields dropped. The server is synchronous and per-client — state that must be shared (rate limits, cookie profiles) is already cross-process safe on disk, so no daemon is needed.

See also

Sibling projects in the rekursiv-ai family:

  • sagent — The self-mutating multi-provider coding-agent CLI and typed Python library.
  • trackinizer — Centralized agent database for tracking inquiries, work, and the evidence behind conclusions.
  • madcatter — Rich-based Markdown renderer for the terminal; ships the mdcat CLI.
  • priml — Composable PyTorch building blocks: models, optimizers, losses, and a step-based training loop.
  • configgle — Hierarchical experiment configuration in typed pure-Python dataclasses instead of YAML.
  • copybarista — Bidirectional source sync for publishing OSS-ready trees from a monorepo.

Citing

If you find our work useful, please consider citing:

@misc{rekursivai2026wesearch,
      title={Wesearch - A wascally web research toolkit for agents.},
      author={Joshua V. Dillon},
      year={2026},
      howpublished={Github},
      url={https://github.com/rekursiv-ai/wesearch},
}

Metadata

Release files for wesearch 0.1.14

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wesearch 0.1.14
File Size Uploaded Provenance Actions
wesearch-0.1.14.tar.gz 530.7 kB With provenance Details for wesearch-0.1.14.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for wesearch 0.1.14
File Interpreter ABI Platform Actions
wesearch-0.1.14-py3-none-any.whl Python 3 none any Details for wesearch-0.1.14-py3-none-any.whl

Total release size: 699.1 kB

Release files / wesearch-0.1.14.tar.gz

Download URL wesearch-0.1.14.tar.gz
Size 530.7 kB
Tags Source
SHA-256 checksum
How to use checksums
0bc9d120d1868142151036bf9fb2c2657030ee1ea7a3bd3bd18d8aadcafc1f3b
BLAKE2b-256 checksum
How to use checksums
04fa0c3c789106beb34f01209f1c84823b4ae1ee50d8fbe4afc3c2036261bde2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / wesearch-0.1.14-py3-none-any.whl

Download URL wesearch-0.1.14-py3-none-any.whl
Size 168.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9760d51b26d4879a59f697dfaf680a8763c0a978c489cf88c24987931ce38e31
BLAKE2b-256 checksum
How to use checksums
6051947a03656c34a2cce88f3cd430fb01a6c676b89495d184f93f52bcaab4b4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for wesearch 0.1.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for wesearch-0.1.14.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page