Skip to main content

web-scout-ai

web-scout-ai logo

PyPI Version PyPI Downloads per Month Python Versions License

Give it a question or a URL. Get back a cited answer, the extracted evidence behind it, and an audit trail of every source the pipeline could—or could not—read.

from web_scout import run_web_research

result = await run_web_research(
    "What are the main climate risks for Kenya's agricultural sector?"
)

print(result.synthesis)
print(f"Read {len(result.scraped)} sources across {len(result.queries)} searches")
Example output (abridged from a recorded project run)

Kenya's agricultural sector is highly exposed because it is approximately 98% rain-fed and most farmers lack irrigation. The evidence identifies five recurring risks:

  • Heat stress: temperatures have risen by about 0.21°C per decade since the 1960s and are projected to rise further by 2050.
  • Less predictable rainfall: regional drying and greater variability disrupt planting and crop growth.
  • More frequent droughts and floods: poor rainy seasons have occurred every 2–3 years since 1999, compared with every 5–6 years previously.
  • Water insecurity: glacier retreat and repeated dry periods threaten agricultural water supplies.
  • Crop and livestock losses: heat and water stress reduce suitable production areas and increase food-security risk.

Sources: World Bank climate profile, Kenya Climate Smart Agriculture Strategy, ILRI, and four more sources.

That run took 95.2 seconds, executed 3 targeted searches, read 7 sources—including 3 PDFs—and returned a 4,471-character synthesis. Results and timing vary with the query, sources, model, and network.

What it actually does

web-scout-ai is an async Python research pipeline, not a search-result wrapper. It searches, opens the selected sources, routes each response to the right extractor, evaluates whether the collected evidence is sufficient, and synthesizes only from sources it successfully read.

Input encountered What the pipeline does
Static HTML Uses a fast HTTP fetch and converts the page to clean Markdown
JS-heavy pages and SPA shells Escalates to a stealth Chromium browser and can interact with page controls
Cloudflare or similar bot challenges Retries with Scrapling's stealth browser and Cloudflare challenge handling
PDF, DOCX, PPTX, XLSX Downloads and converts the document with Docling
Scanned PDFs, charts, maps, images Uses the configured vision model when text extraction is insufficient
JSON endpoints Converts structured payloads into readable evidence
List, index, and database pages Ranks detail links, follows them, and can take one pagination hop
Thin or incomplete evidence Scrapes promising backlog URLs or generates targeted follow-up searches

The browser fallback can pass many JS-gated and Cloudflare-protected pages, but it is not a guarantee: sites can still block automation. Those URLs are reported in result.bot_detected instead of disappearing or being presented as evidence. Use the package only where you have permission and in accordance with the source site's terms.

Quick start

1. Install

pip install web-scout-ai
web-scout-setup

web-scout-setup installs the Patchright-managed Chromium browser and its system dependencies. It may request sudo for OS-level browser libraries.

2. Configure keys

The default models use Gemini and open-web discovery uses Serper:

export GEMINI_API_KEY="your-gemini-api-key"
export SERPER_API_KEY="your-serper-api-key"

Direct-URL mode does not use Serper, so it only needs the API key for your configured model provider.

3. Run research

import asyncio

from web_scout import run_web_research


async def main():
    result = await run_web_research(
        query="What are the main threats to coral reefs worldwide?",
        cache=True,
    )

    print(result.synthesis)

    print("\nSources read:")
    for source in result.scraped:
        print(f"- {source.title or source.url}: {source.url}")

    if result.bot_detected or result.scrape_failed:
        print(
            f"\nCould not read "
            f"{len(result.bot_detected) + len(result.scrape_failed)} source(s)"
        )


asyncio.run(main())

Three ways to use it

Open-web research

Generate several searches, read the strongest results in parallel, evaluate coverage, and search again if important evidence is missing.

result = await run_web_research(
    query="What is driving the global adaptation finance gap?",
)

Domain-restricted research

Keep discovery and hub deepening focused on authoritative domains.

result = await run_web_research(
    query="Latest evidence on sea-level rise",
    include_domains=["ipcc.ch", "nasa.gov"],
)

Direct URL extraction

Skip search and start from a page, document, API endpoint, image, or database listing.

result = await run_web_research(
    query="Extract the recommended adaptation measures and supporting evidence",
    direct_url="https://example.org/report.pdf",
)

For a document, the pipeline reads that document without wandering into site navigation. For a list or database page, it can rank and follow relevant records and their linked primary documents.

The return value is an audit trail

run_web_research() returns a typed WebResearchResult:

class WebResearchResult(BaseModel):
    synthesis: str
    scraped: list[UrlEntry]
    scrape_failed: list[UrlEntry]
    blocked_by_policy: list[UrlEntry]
    source_http_error: list[UrlEntry]
    scraped_irrelevant: list[UrlEntry]
    bot_detected: list[UrlEntry]
    snippet_only: list[UrlEntry]
    queries: list[SearchQuery]
Field Meaning
synthesis Final answer with inline Markdown citations
scraped Sources successfully read and extracted; content contains the query-relevant evidence
scrape_failed Extraction attempts that failed for an unclassified reason
blocked_by_policy Sources skipped by exclude_domains (defaults to BLOCKED_DOMAINS)
source_http_error Source-side HTTP or network failures
scraped_irrelevant Pages fetched successfully but not useful for the query
bot_detected Sources that still returned a bot-protection wall
snippet_only Search results discovered but not opened; snippets are never valid citation targets
queries Every search query executed, its result count, and domain restrictions

UrlEntry contains url, title, and content. SearchQuery contains query, num_results_returned, and domains_restricted.

The synthesizer is instructed to use scraped evidence only. A deterministic final check rejects citations to invented or snippet-only URLs and retries the synthesis with feedback.

Research depth

# Faster default
await run_web_research(query="...", research_depth="standard")

# More searches and sources, with a stricter coverage threshold
await run_web_research(query="...", research_depth="deep")
Budget Standard Deep
Maximum search iterations 2 3
Initial search queries 3 5
Follow-up search queries 2 4
URLs selected in the first round 6 12
URLs selected in a follow-up round 4 8
Hub/detail-page cap 10 15

The coverage evaluator can stop early when the evidence already answers the question. You can add your own acceptance conditions with coverage_criteria:

result = await run_web_research(
    query="Compare national methane policies",
    coverage_criteria="Include at least one primary government source per country.",
)

Custom extractor guidance

Use extractor_guidance to refine what each per-source extractor keeps and how it organizes evidence. The guidance applies only to extraction; it does not alter search query generation, coverage evaluation, or final synthesis.

result = await run_web_research(
    query="Tunisia agricultural trends",
    extractor_guidance="""
Country of interest: Tunisia

- Keep findings explicitly about Tunisia.
- Also keep regional findings whose stated scope includes Tunisia, even when the
  source does not name Tunisia; label them with the source's scope, such as
  "North Africa:".
- Discard purely global findings and findings about geographies that do not
  include Tunisia.
""",
)

Guidance augments rather than replaces WebScout's extractor instructions. The base contract always takes precedence for source grounding, tool constraints, output fields, page-type handling, and the exact no-evidence sentinel.

Configuration

The defaults use gemini/gemini-3-flash-preview. Model IDs follow LiteLLM provider naming, so the research and extraction stages can use different providers or models.

models = {
    # Used as the fallback for query generation, coverage, and synthesis
    "web_researcher": "openai/gpt-4o-mini",

    # Reads and extracts individual sources
    "content_extractor": "gemini/gemini-2.0-flash",

    # Optional stage-specific choices
    "query_generator": "openai/gpt-4o-mini",
    "coverage_evaluator": "openai/gpt-4o-mini",
    "synthesiser": "openai/gpt-4o-mini",
    "followup_selector": "openai/gpt-4o-mini",
    "vision_fallback": "gemini/gemini-2.0-flash",
}

result = await run_web_research(query="...", models=models)

Provider credentials are read from their standard environment variables, such as OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, or AWS credentials for Bedrock.

Public API

result = await run_web_research(
    query="latest IPCC findings on sea-level rise",
    models=None,                       # optional; Gemini defaults
    search_backend="serper",          # currently supported search backend
    research_depth="standard",        # "standard", "deep", or a custom dict
    include_domains=["ipcc.ch"],       # optional discovery restriction
    direct_url=None,                   # optional; skips search when set
    domain_expertise="climate science",  # optional prompt context
    exclude_domains=None,              # blocklist; defaults to BLOCKED_DOMAINS
    max_pdf_pages=50,                  # pages converted from each PDF
    max_content_chars=30_000,          # characters passed to the extractor per source
    cache=False,                       # process-local source cache
    coverage_criteria=None,            # extra evidence requirements
    extractor_guidance=None,           # optional per-source extraction guidance
)

Domain policy

Common social, video, and consistently paywalled platforms are blocked by default so they do not consume the scrape budget. The blocklist is exclude_domains, which defaults to BLOCKED_DOMAINS. Pass a custom list to replace it, drop entries from the default list to opt a domain back in, or pass [] to disable domain blocking:

from web_scout.scraping.constants import BLOCKED_DOMAINS

# Opt reddit.com back in while keeping the rest of the default blocklist
result = await run_web_research(
    query="...",
    exclude_domains=sorted(BLOCKED_DOMAINS - {"reddit.com"}),
)

# Disable domain blocking entirely
result = await run_web_research(
    query="...",
    exclude_domains=[],
)

Domains listed in include_domains, and the host of direct_url, are automatically removed from the effective exclude set so those targets remain reachable.

Source caching

With cache=True, successful raw source artifacts are reused by later run_web_research() calls in the same Python process. Pages and documents are not fetched or converted again, but query-specific extraction and synthesis still run each time.

The cache is in-memory only. Failed scrapes, final answers, query-specific summaries, and click-driven browser sessions are not cached.

Pipeline, in one view

question
   │
   ├─ generate targeted searches ─ search in parallel ─ select diverse URLs
   │                                                     │
   │                                                     ▼
   │      static HTML ────────┐                    fetch in parallel
   │      JS / bot challenge ─┤                          │
   │      documents ──────────┼─ route + extract ◀───────┘
   │      JSON / images ──────┘          │
   │                                    ▼
   └─ follow-up search ◀── evaluate evidence coverage
                                        │ sufficient
                                        ▼
                              grounded synthesis
                                        │
                                        ▼
                               citation validation

For the maintained control-flow diagrams and exact routing rules, see docs/pipeline-flow.md.

Where it fits

Use web-scout-ai when your application needs the contents of real pages and documents—not only search snippets—and you want the source successes and failures returned as structured data.

It is intentionally a bounded research component. If you only need search links, use a search API directly. If you need an open-ended autonomous research process with human checkpoints, put this package inside a broader agent workflow.

Requirements

  • Python 3.10–3.13
  • An API key for the configured LLM provider
  • A Serper API key for search mode
  • Chromium setup for rendered pages, interactive sites, and browser fallbacks

Contributing

The main extension point is SearchBackend. New backends should implement the async search() contract and return normalized results and related searches.

Bug reports and focused pull requests are welcome at github.com/RSO9192/web-scout-ai.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

web_scout_ai-1.4.1.tar.gz (96.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

web_scout_ai-1.4.1-py3-none-any.whl (113.9 kB view details)

Uploaded Python 3

File details

Details for the file web_scout_ai-1.4.1.tar.gz.

File metadata

  • Download URL: web_scout_ai-1.4.1.tar.gz
  • Upload date:
  • Size: 96.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.2 CPython/3.14.2 Darwin/25.5.0

File hashes

Hashes for web_scout_ai-1.4.1.tar.gz
Algorithm Hash digest
SHA256 958a0458c0021b44ec29ee2e1436a820a35db68ae339dc4972c3af6f81eac081
MD5 2c843e25ee7eaab40b7c6c7d31abad92
BLAKE2b-256 b7c0978eb53309fb42d5e97efa7f35fc75d4c6f9a280b971fb52b57119df2069

See more details on using hashes here.

File details

Details for the file web_scout_ai-1.4.1-py3-none-any.whl.

File metadata

  • Download URL: web_scout_ai-1.4.1-py3-none-any.whl
  • Upload date:
  • Size: 113.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.2 CPython/3.14.2 Darwin/25.5.0

File hashes

Hashes for web_scout_ai-1.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fd9b7f57912b13a0f7ca7e0187522f6ff975d0c6ed141494731dc3077ed6a82b
MD5 4a3f23222891329318d5a08c9cc0c432
BLAKE2b-256 9ed658211587c11d73270b143b6750ad7e98a3a3650fb7920796e21a2f98c2ab

See more details on using hashes here.

Release history Release notifications | RSS feed

1.4.2

2 files

This release

1.4.1 This release

2 files

1.4.0

2 files

1.3.10

2 files

1.3.9

2 files

1.3.8

2 files

1.3.6

2 files

1.3.5

2 files

1.3.3

2 files

1.3.2

2 files

1.3.1

2 files

1.3.0

2 files

1.2.2

2 files

1.2.1

2 files

1.2.0

2 files

1.1.1

2 files

1.1.0

2 files

1.0.5

2 files

1.0.3

2 files

0.9.4

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page