webfetch-llm 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webfetch_llm-0.1.0/.claude/settings.local.json +45 -0
- webfetch_llm-0.1.0/.env.example +34 -0
- webfetch_llm-0.1.0/.github/workflows/ci.yml +38 -0
- webfetch_llm-0.1.0/.github/workflows/publish.yml +38 -0
- webfetch_llm-0.1.0/.gitignore +23 -0
- webfetch_llm-0.1.0/CHANGELOG.md +24 -0
- webfetch_llm-0.1.0/CONTRIBUTING.md +34 -0
- webfetch_llm-0.1.0/LICENSE +21 -0
- webfetch_llm-0.1.0/PKG-INFO +288 -0
- webfetch_llm-0.1.0/README.md +235 -0
- webfetch_llm-0.1.0/claude.md +57 -0
- webfetch_llm-0.1.0/docs/ROADMAP.md +151 -0
- webfetch_llm-0.1.0/docs/architecture.md +661 -0
- webfetch_llm-0.1.0/docs/context.md +102 -0
- webfetch_llm-0.1.0/evals/build_datasets.py +362 -0
- webfetch_llm-0.1.0/evals/common.py +147 -0
- webfetch_llm-0.1.0/evals/datasets/compression_chunks.jsonl +50 -0
- webfetch_llm-0.1.0/evals/datasets/fresh_queries_2026_07.jsonl +27 -0
- webfetch_llm-0.1.0/evals/datasets/fresh_queries_2026_07.meta.json +19 -0
- webfetch_llm-0.1.0/evals/datasets/freshqa_queries.jsonl +377 -0
- webfetch_llm-0.1.0/evals/datasets/freshqa_queries.meta.json +12 -0
- webfetch_llm-0.1.0/evals/datasets/live_queries.jsonl +65 -0
- webfetch_llm-0.1.0/evals/datasets/live_queries.meta.json +15 -0
- webfetch_llm-0.1.0/evals/datasets/matcher_pairs.jsonl +247 -0
- webfetch_llm-0.1.0/evals/datasets/matcher_pairs.meta.json +20 -0
- webfetch_llm-0.1.0/evals/fetch_datasets.py +96 -0
- webfetch_llm-0.1.0/evals/run_compression_eval.py +319 -0
- webfetch_llm-0.1.0/evals/run_e2e_eval.py +732 -0
- webfetch_llm-0.1.0/evals/run_matcher_eval.py +399 -0
- webfetch_llm-0.1.0/evals/run_pipeline_eval.py +477 -0
- webfetch_llm-0.1.0/evals/run_volatility_eval.py +214 -0
- webfetch_llm-0.1.0/evals/test_metrics.py +62 -0
- webfetch_llm-0.1.0/examples/agent_loop.py +115 -0
- webfetch_llm-0.1.0/pyproject.toml +107 -0
- webfetch_llm-0.1.0/requirements.txt +31 -0
- webfetch_llm-0.1.0/tests/test_cache.py +74 -0
- webfetch_llm-0.1.0/tests/test_chunker.py +36 -0
- webfetch_llm-0.1.0/tests/test_compress.py +101 -0
- webfetch_llm-0.1.0/tests/test_context.py +38 -0
- webfetch_llm-0.1.0/tests/test_receipts.py +28 -0
- webfetch_llm-0.1.0/tests/test_resilience.py +103 -0
- webfetch_llm-0.1.0/tests/test_tool.py +87 -0
- webfetch_llm-0.1.0/webfetch/__init__.py +48 -0
- webfetch_llm-0.1.0/webfetch/cache.py +347 -0
- webfetch_llm-0.1.0/webfetch/compress.py +360 -0
- webfetch_llm-0.1.0/webfetch/config.py +154 -0
- webfetch_llm-0.1.0/webfetch/data/volatility_centroids.json +1 -0
- webfetch_llm-0.1.0/webfetch/extract/__init__.py +35 -0
- webfetch_llm-0.1.0/webfetch/extract/base.py +180 -0
- webfetch_llm-0.1.0/webfetch/extract/claude.py +71 -0
- webfetch_llm-0.1.0/webfetch/extract/gemini.py +89 -0
- webfetch_llm-0.1.0/webfetch/extract/gpt.py +70 -0
- webfetch_llm-0.1.0/webfetch/extract/groq.py +83 -0
- webfetch_llm-0.1.0/webfetch/fetch/__init__.py +89 -0
- webfetch_llm-0.1.0/webfetch/fetch/base.py +42 -0
- webfetch_llm-0.1.0/webfetch/fetch/html.py +251 -0
- webfetch_llm-0.1.0/webfetch/fetch/pdf.py +246 -0
- webfetch_llm-0.1.0/webfetch/mcp.py +68 -0
- webfetch_llm-0.1.0/webfetch/pipeline.py +245 -0
- webfetch_llm-0.1.0/webfetch/rank/__init__.py +98 -0
- webfetch_llm-0.1.0/webfetch/rank/base.py +52 -0
- webfetch_llm-0.1.0/webfetch/rank/biencoder.py +91 -0
- webfetch_llm-0.1.0/webfetch/rank/bm25.py +70 -0
- webfetch_llm-0.1.0/webfetch/rank/chunker.py +78 -0
- webfetch_llm-0.1.0/webfetch/rank/crossencoder.py +73 -0
- webfetch_llm-0.1.0/webfetch/rank/hybrid.py +88 -0
- webfetch_llm-0.1.0/webfetch/rank/rrf.py +64 -0
- webfetch_llm-0.1.0/webfetch/receipts.py +123 -0
- webfetch_llm-0.1.0/webfetch/search/__init__.py +104 -0
- webfetch_llm-0.1.0/webfetch/search/base.py +56 -0
- webfetch_llm-0.1.0/webfetch/search/brave.py +91 -0
- webfetch_llm-0.1.0/webfetch/search/ddg.py +67 -0
- webfetch_llm-0.1.0/webfetch/search/multi.py +145 -0
- webfetch_llm-0.1.0/webfetch/search/resilience.py +153 -0
- webfetch_llm-0.1.0/webfetch/search/serper.py +90 -0
- webfetch_llm-0.1.0/webfetch/search/tavily.py +74 -0
- webfetch_llm-0.1.0/webfetch/semcache.py +270 -0
- webfetch_llm-0.1.0/webfetch/tool.py +190 -0
- webfetch_llm-0.1.0/webfetch/volatility.py +101 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(python3 -c ' *)",
|
|
5
|
+
"WebFetch(domain:en.wikipedia.org)",
|
|
6
|
+
"Bash(git commit -m ' *)",
|
|
7
|
+
"Bash(pip3 list *)",
|
|
8
|
+
"Bash(python3 -c \"import ddgs; print\\('ddgs', ddgs.__version__\\)\")",
|
|
9
|
+
"Bash(python3 -c \"import duckduckgo_search as d; print\\('duckduckgo_search', d.__version__\\)\")",
|
|
10
|
+
"WebFetch(domain:developers.openai.com)",
|
|
11
|
+
"Bash(python3 -u evals/run_e2e_eval.py --arms ours-gpt,openai-hosted,broke --limit 1)",
|
|
12
|
+
"Bash(python3 -u evals/run_e2e_eval.py --arms ours-gpt,openai-hosted --limit 2)",
|
|
13
|
+
"Bash(python3 -u evals/run_e2e_eval.py --arms ours-gpt --limit 2)",
|
|
14
|
+
"Bash(python3 -u evals/run_e2e_eval.py --arms ours-multi,ours-ddg,ours-gpt,hosted,openai-hosted,tavily,exa,sonar,broke)",
|
|
15
|
+
"Bash(python3 -u evals/run_e2e_eval.py --dataset evals/datasets/fresh_queries_2026_07.jsonl --arms ours-multi,ours-ddg,ours-gpt,hosted,openai-hosted,tavily,exa,sonar,broke)",
|
|
16
|
+
"Bash(python3 -u evals/run_e2e_eval.py --arms broke)",
|
|
17
|
+
"Bash(python3 -u evals/run_e2e_eval.py --dataset evals/datasets/fresh_queries_2026_07.jsonl --arms broke)",
|
|
18
|
+
"Bash(pkill -f \"run_e2e_eval.py --arms broke\")",
|
|
19
|
+
"Bash(python3 -u evals/run_e2e_eval.py --arms ours-haiku --limit 2)",
|
|
20
|
+
"Bash(python3 -u evals/run_e2e_eval.py --arms ours-haiku)",
|
|
21
|
+
"Bash(python3 -u evals/run_e2e_eval.py --dataset evals/datasets/fresh_queries_2026_07.jsonl --arms ours-haiku)",
|
|
22
|
+
"Bash(python3 -c \"from webfetch import savings_report, get_counters, Pipeline; print\\('import smoke ok'\\)\")",
|
|
23
|
+
"Bash(python3 -m pytest tests/ -q)",
|
|
24
|
+
"Bash(python3 -m build --wheel)",
|
|
25
|
+
"Bash(/tmp/webfetch_venv_all/bin/pip install *)",
|
|
26
|
+
"Bash(/tmp/webfetch_venv_all/bin/webfetch-savings)",
|
|
27
|
+
"Bash(env -i HOME=/tmp/webfetch_nokey PATH=/usr/bin:/bin /tmp/webfetch_venv_all/bin/python -c ' *)",
|
|
28
|
+
"Bash(git tag *)",
|
|
29
|
+
"Bash(git fetch *)",
|
|
30
|
+
"Bash(env -u GITHUB_TOKEN gh auth status)",
|
|
31
|
+
"Bash(env -u GITHUB_TOKEN gh repo edit firish/webfetch --visibility public --accept-visibility-change-consequences)",
|
|
32
|
+
"Bash(env -u GITHUB_TOKEN gh repo view firish/webfetch --json visibility,description -q '.visibility + \" | \" + .description')",
|
|
33
|
+
"Bash(env -u GITHUB_TOKEN gh repo edit firish/webfetch --description \"Own your LLM's web search: a local search->fetch->rank pipeline that replaces hosted web_search tools. Measured: matches hosted accuracy at ~1/3 the cost with 5-8x fewer tokens, plus semantic caching no API offers.\" --add-topic llm --add-topic web-search --add-topic agents --add-topic mcp --add-topic rag --add-topic anthropic --add-topic openai --add-topic semantic-cache)",
|
|
34
|
+
"Bash(env -u GITHUB_TOKEN gh release create v0.1.0 --draft --title 'webfetch v0.1.0' --notes ' *)",
|
|
35
|
+
"Bash(env -u GITHUB_TOKEN gh run list --repo firish/webfetch --limit 4)",
|
|
36
|
+
"Bash(python3 -m build)",
|
|
37
|
+
"Bash(python3 -m twine check dist/*)",
|
|
38
|
+
"Bash(curl -s -o /dev/null -w '%{http_code}' https://pypi.org/simple/__TRACKED_VAR__/)",
|
|
39
|
+
"Bash(read f *)"
|
|
40
|
+
],
|
|
41
|
+
"additionalDirectories": [
|
|
42
|
+
"/Users/rishisg2/Desktop/Rishi/coding/prototypes/websearch"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# webfetch API keys. Everything here is OPTIONAL - the library works with
|
|
2
|
+
# zero keys (DuckDuckGo needs none). Each key you add unlocks another
|
|
3
|
+
# search engine for fusion/fallback, which improves recall.
|
|
4
|
+
# Copy to .env; python-dotenv loads it in the examples and evals
|
|
5
|
+
# (the library itself never reads .env - export vars or use dotenv yourself).
|
|
6
|
+
|
|
7
|
+
# --- Search engines (all have free tiers) ---
|
|
8
|
+
# Brave Search API - free 2,000 queries/month: https://brave.com/search/api/
|
|
9
|
+
BRAVE_API_KEY=
|
|
10
|
+
|
|
11
|
+
# Serper (Google results) - free trial credits: https://serper.dev
|
|
12
|
+
SERPER_API_KEY=
|
|
13
|
+
|
|
14
|
+
# Tavily - free 1,000 credits/month: https://tavily.com
|
|
15
|
+
TAVILY_API_KEY=
|
|
16
|
+
|
|
17
|
+
# --- LLM providers (for the agent-loop examples and evals) ---
|
|
18
|
+
# Anthropic - examples/agent_loop.py and the eval judge
|
|
19
|
+
ANTHROPIC_API_KEY=
|
|
20
|
+
|
|
21
|
+
# OpenAI - the ours-gpt / openai-hosted eval arms
|
|
22
|
+
OPENAI_API_KEY=
|
|
23
|
+
|
|
24
|
+
# Groq - free tier; drives the $0 "broke" eval arm: https://console.groq.com
|
|
25
|
+
GROQ_API_KEY=
|
|
26
|
+
|
|
27
|
+
# Google Gemini - alternate free-tier model for the broke arm
|
|
28
|
+
GOOGLE_API_KEY=
|
|
29
|
+
|
|
30
|
+
# Perplexity - the sonar eval arm only
|
|
31
|
+
PERPLEXITY_API_KEY=
|
|
32
|
+
|
|
33
|
+
# Exa - the exa eval arm only: https://exa.ai
|
|
34
|
+
EXA_API_KEY=
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
# Core deps only: the library must import and its offline tests must pass
|
|
10
|
+
# without any optional extra - graceful degradation is a feature contract.
|
|
11
|
+
test-core:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
cache: pip
|
|
22
|
+
- run: pip install -e .[dev]
|
|
23
|
+
- run: ruff check webfetch/ tests/
|
|
24
|
+
- run: python -c "import webfetch"
|
|
25
|
+
- run: pytest tests/ -q
|
|
26
|
+
|
|
27
|
+
# Everything installed (torch included) - same tests, no degradation paths.
|
|
28
|
+
test-all-extras:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.12"
|
|
35
|
+
cache: pip
|
|
36
|
+
- run: pip install -e .[all,dev]
|
|
37
|
+
- run: pytest tests/ -q
|
|
38
|
+
- run: python evals/test_metrics.py
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI via Trusted Publishing (OIDC - no token stored in the
|
|
4
|
+
# repo) whenever a GitHub Release is published. One-time setup on PyPI:
|
|
5
|
+
# add a "pending publisher" for this repo + workflow name under
|
|
6
|
+
# https://pypi.org/manage/account/publishing/
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
release:
|
|
10
|
+
types: [published]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- run: pip install build
|
|
21
|
+
- run: python -m build
|
|
22
|
+
- uses: actions/upload-artifact@v4
|
|
23
|
+
with:
|
|
24
|
+
name: dist
|
|
25
|
+
path: dist/
|
|
26
|
+
|
|
27
|
+
publish:
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment: pypi
|
|
31
|
+
permissions:
|
|
32
|
+
id-token: write
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/download-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Test File
|
|
2
|
+
test_file.py
|
|
3
|
+
|
|
4
|
+
# Secrets
|
|
5
|
+
.env
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.pyc
|
|
10
|
+
*.pyo
|
|
11
|
+
.venv/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
|
|
14
|
+
# Dev readme files
|
|
15
|
+
learning.md
|
|
16
|
+
# Eval harness: raw downloads and run outputs
|
|
17
|
+
evals/datasets/raw/
|
|
18
|
+
evals/results/
|
|
19
|
+
|
|
20
|
+
# Build artifacts
|
|
21
|
+
dist/
|
|
22
|
+
build/
|
|
23
|
+
*.egg-info/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-07-14
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `web_search` tool for LLM agent loops (Anthropic tool schema; OpenAI
|
|
8
|
+
function-calling needs only a mechanical reshape) with a crash-proof
|
|
9
|
+
handler: all failures return readable strings, never exceptions
|
|
10
|
+
- Pipeline: multi-engine search fusion (DDG/Brave/Serper/Tavily via
|
|
11
|
+
reciprocal rank fusion), concurrent fetch with extraction fallback chain
|
|
12
|
+
(trafilatura -> readability -> newspaper4k -> Playwright), hybrid
|
|
13
|
+
BM25 + bi-encoder ranking with cross-encoder reranking
|
|
14
|
+
- Sentence-level compression of tool results: cross-encoder scored,
|
|
15
|
+
measured 50% token reduction at zero recall loss
|
|
16
|
+
- Two-level sqlite cache (pages by URL, ranked chunks by query) with
|
|
17
|
+
semantic paraphrase matching (bi-encoder shortlist + NLI verification)
|
|
18
|
+
and volatility-aware TTLs (realtime/recent/stable)
|
|
19
|
+
- Search resilience: per-engine circuit breakers, silent-block detection,
|
|
20
|
+
priority-failover "fallback" provider
|
|
21
|
+
- Cost receipts: lifetime counters in the cache db, `webfetch-savings` CLI
|
|
22
|
+
- MCP server (`webfetch-mcp`) for Claude Code/Desktop and other clients
|
|
23
|
+
- Three-layer eval harness with checked-in datasets, including a
|
|
24
|
+
contamination-resistant fresh-events set; benchmark results in README
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Dev setup:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
git clone https://github.com/firish/webfetch && cd webfetch
|
|
7
|
+
pip install -e ".[all,dev]"
|
|
8
|
+
pytest tests/ -q # offline, no keys needed, ~1s
|
|
9
|
+
ruff check webfetch/ tests/
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The one rule this project actually enforces: **features ship eval-first**.
|
|
13
|
+
Every non-trivial change to retrieval, ranking, caching, or compression
|
|
14
|
+
needs a measured gate before it merges - build or extend an eval in
|
|
15
|
+
`evals/`, state the acceptance number up front, and report the result
|
|
16
|
+
honestly (negative results are documented in `docs/ROADMAP.md`, not
|
|
17
|
+
deleted). Look at `evals/run_compression_eval.py` for the shape: capture
|
|
18
|
+
once, sweep offline, gate, and the eval imports the shipped module so what
|
|
19
|
+
is measured is what ships.
|
|
20
|
+
|
|
21
|
+
Smaller conventions, mostly enforced by review:
|
|
22
|
+
|
|
23
|
+
- Every major stage (search, fetch, rank, extract, cache) is an abstract
|
|
24
|
+
base with swappable adapters. A new search provider is a new file plus a
|
|
25
|
+
registry entry in `webfetch/search/__init__.py` - `pipeline.py` never
|
|
26
|
+
changes.
|
|
27
|
+
- Heavy dependencies stay optional. Code that needs them degrades with a
|
|
28
|
+
logged warning, not an ImportError.
|
|
29
|
+
- No hardcoded values - tunables live in `webfetch/config.py`.
|
|
30
|
+
- Keep `docs/architecture.md` current when interfaces or data flow change.
|
|
31
|
+
|
|
32
|
+
Live evals cost real API money and take 20+ minutes; maintainers run
|
|
33
|
+
those. For PRs, the offline suite plus a clear description of what you
|
|
34
|
+
measured is enough.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rishi Gulati
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webfetch-llm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Own your LLM's web search: a local search -> fetch -> rank pipeline that replaces hosted web_search tools at a fraction of the cost
|
|
5
|
+
Project-URL: Homepage, https://github.com/firish/webfetch
|
|
6
|
+
Project-URL: Repository, https://github.com/firish/webfetch
|
|
7
|
+
Project-URL: Issues, https://github.com/firish/webfetch/issues
|
|
8
|
+
Author: Rishi Gulati
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,anthropic,llm,mcp,openai,rag,semantic-cache,tool-use,web-search
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: anthropic>=0.25
|
|
22
|
+
Requires-Dist: ddgs>=6.0
|
|
23
|
+
Requires-Dist: newspaper4k>=0.9
|
|
24
|
+
Requires-Dist: rank-bm25>=0.2.2
|
|
25
|
+
Requires-Dist: readability-lxml>=0.8
|
|
26
|
+
Requires-Dist: requests>=2.31
|
|
27
|
+
Requires-Dist: trafilatura>=1.8
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: mcp>=1.0; extra == 'all'
|
|
30
|
+
Requires-Dist: numpy>=1.26; extra == 'all'
|
|
31
|
+
Requires-Dist: pandas>=2.0; extra == 'all'
|
|
32
|
+
Requires-Dist: pdfplumber>=0.11; extra == 'all'
|
|
33
|
+
Requires-Dist: playwright>=1.44; extra == 'all'
|
|
34
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'all'
|
|
35
|
+
Requires-Dist: tabulate>=0.9; extra == 'all'
|
|
36
|
+
Provides-Extra: browser
|
|
37
|
+
Requires-Dist: playwright>=1.44; extra == 'browser'
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
42
|
+
Provides-Extra: mcp
|
|
43
|
+
Requires-Dist: mcp>=1.0; extra == 'mcp'
|
|
44
|
+
Provides-Extra: pdf
|
|
45
|
+
Requires-Dist: pdfplumber>=0.11; extra == 'pdf'
|
|
46
|
+
Provides-Extra: rerank
|
|
47
|
+
Requires-Dist: numpy>=1.26; extra == 'rerank'
|
|
48
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'rerank'
|
|
49
|
+
Provides-Extra: tables
|
|
50
|
+
Requires-Dist: pandas>=2.0; extra == 'tables'
|
|
51
|
+
Requires-Dist: tabulate>=0.9; extra == 'tables'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# webfetch
|
|
55
|
+
|
|
56
|
+
Web search for LLM agents that you run yourself.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
pip install webfetch-llm
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
(The PyPI name is `webfetch-llm`; the import is `webfetch`.)
|
|
63
|
+
|
|
64
|
+
Hosted web-search tools charge $10 per thousand searches (Anthropic and
|
|
65
|
+
OpenAI both, as of July 2026) and then bill you again for every token of
|
|
66
|
+
retrieved content they stuff into your context window - about 17,000 input
|
|
67
|
+
tokens per search in our measurements. webfetch replaces that with a local
|
|
68
|
+
pipeline: multi-engine search, page fetching and extraction, semantic
|
|
69
|
+
reranking, and sentence-level compression, exposed as a `web_search` tool
|
|
70
|
+
your model calls like any other. You pay for the tokens of a compressed,
|
|
71
|
+
ranked result (~2-3.5k) and, optionally, a fraction of a cent in search
|
|
72
|
+
engine fees. Repeated and paraphrased queries are served from a semantic
|
|
73
|
+
cache and cost nothing at all.
|
|
74
|
+
|
|
75
|
+
The pipeline was built eval-first: every stage shipped behind a measured
|
|
76
|
+
gate, and the numbers below come from a benchmark you can rerun from this
|
|
77
|
+
repo.
|
|
78
|
+
|
|
79
|
+
## The numbers
|
|
80
|
+
|
|
81
|
+
Same agent loop, same 50 SimpleQA questions, same judge. Only the search
|
|
82
|
+
tool changes. Cost per query includes model tokens and all search fees.
|
|
83
|
+
|
|
84
|
+
| tool backend | model | accuracy | cost/query | input tok/query |
|
|
85
|
+
|---|---|---|---|---|
|
|
86
|
+
| webfetch (4-engine fusion) | gpt-5.6-sol | 96% | $0.040 | 2,156 |
|
|
87
|
+
| OpenAI hosted web_search | gpt-5.6-sol | 100% | $0.066 | 10,027 |
|
|
88
|
+
| Anthropic hosted web_search | Opus 4.7 | 96% | $0.108 | 17,408 |
|
|
89
|
+
| webfetch (4-engine fusion) | Opus 4.7 | 92% | $0.035 | 3,467 |
|
|
90
|
+
| Exa (search + contents) | Opus 4.7 | 90% | $0.053 | 5,496 |
|
|
91
|
+
| Tavily | Opus 4.7 | 88% | $0.047 | 6,387 |
|
|
92
|
+
| webfetch (DDG only, $0 fees) | Opus 4.7 | 84% | $0.026 | 3,623 |
|
|
93
|
+
| webfetch (4-engine fusion) | Haiku 4.5 | 76%* | $0.031 | 3,021 |
|
|
94
|
+
|
|
95
|
+
\* the Haiku run's failures were mostly the model re-searching past the
|
|
96
|
+
turn cap, and its last few questions ran with a degraded engine set after
|
|
97
|
+
we exhausted a free tier mid-benchmark. Treat it as a floor.
|
|
98
|
+
|
|
99
|
+
On a second dataset of 27 questions about events from the two weeks before
|
|
100
|
+
the benchmark ran (hand-written, never published, so no vendor could have
|
|
101
|
+
tuned on them), webfetch scored 100% with fusion and 100% with DDG alone;
|
|
102
|
+
the hosted tools also scored 100%. Fresh events are not the hard part -
|
|
103
|
+
see the date-injection note below, which is the actual trap.
|
|
104
|
+
|
|
105
|
+
Two structural advantages don't show in a single-query benchmark:
|
|
106
|
+
|
|
107
|
+
- Caching. Identical queries hit an exact cache; paraphrased queries hit a
|
|
108
|
+
semantic cache (embedding shortlist, then NLI verification - measured
|
|
109
|
+
11/11 paraphrase hits with zero wrong matches). Cache hits skip engines
|
|
110
|
+
and fetching entirely. No hosted tool or search API we surveyed offers
|
|
111
|
+
any client-visible caching, let alone paraphrase-aware caching.
|
|
112
|
+
- Token efficiency compounds. Every result webfetch returns is roughly a
|
|
113
|
+
fifth of what hosted search injects, and in a multi-search agent
|
|
114
|
+
conversation that difference is paid on every subsequent turn.
|
|
115
|
+
|
|
116
|
+
Reproduce: `python evals/run_e2e_eval.py --arms ours-multi,hosted` (see
|
|
117
|
+
[evals/](evals/) for the harness, datasets, and per-question records).
|
|
118
|
+
|
|
119
|
+
## Install
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
pip install webfetch-llm # core: search, fetch, BM25 ranking, cache
|
|
123
|
+
pip install "webfetch-llm[all]" # + semantic rerank/cache/compression, JS
|
|
124
|
+
# rendering, PDF, tables, MCP server
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Needs pip >= 24 (`python -m pip install -U pip` first in a fresh venv -
|
|
128
|
+
older pips crash on a duplicated extra in our dependency tree).
|
|
129
|
+
|
|
130
|
+
The core install works with zero API keys - DuckDuckGo needs none. Add
|
|
131
|
+
keys to unlock more engines (all have free tiers) and better recall:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
cp .env.example .env # then fill in what you have
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The benchmark numbers above use the 4-engine fusion config
|
|
138
|
+
(DDG + Brave + Serper + Tavily) with the `[rerank]` extra installed.
|
|
139
|
+
|
|
140
|
+
## Use it in an agent loop
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import time
|
|
144
|
+
import anthropic
|
|
145
|
+
from webfetch import WEB_SEARCH_TOOL, handle_web_search
|
|
146
|
+
|
|
147
|
+
client = anthropic.Anthropic()
|
|
148
|
+
messages = [{"role": "user", "content": "What did the FOMC decide this week?"}]
|
|
149
|
+
system = (f"Today's date is {time.strftime('%Y-%m-%d')}. "
|
|
150
|
+
"Use web_search for recent facts.")
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
response = client.messages.create(
|
|
154
|
+
model="claude-opus-4-7", max_tokens=2000, system=system,
|
|
155
|
+
tools=[WEB_SEARCH_TOOL], messages=messages,
|
|
156
|
+
)
|
|
157
|
+
if response.stop_reason != "tool_use":
|
|
158
|
+
break
|
|
159
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
160
|
+
results = [{"type": "tool_result", "tool_use_id": b.id,
|
|
161
|
+
"content": handle_web_search(b.input)}
|
|
162
|
+
for b in response.content if b.type == "tool_use"]
|
|
163
|
+
messages.append({"role": "user", "content": results})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A complete version with prompt caching and adaptive thinking is in
|
|
167
|
+
[examples/agent_loop.py](examples/agent_loop.py).
|
|
168
|
+
|
|
169
|
+
**Put today's date in your system prompt.** This is not optional. Models
|
|
170
|
+
refuse to search for events they believe haven't happened yet: on our
|
|
171
|
+
fresh-events dataset, arms without the date declined to even call the tool
|
|
172
|
+
on up to 10 of 27 questions ("Wimbledon 2026 hasn't taken place yet").
|
|
173
|
+
One line fixes it. Hosted search tools do this server-side, which is part
|
|
174
|
+
of why nobody notices until they run their own tool.
|
|
175
|
+
|
|
176
|
+
`handle_web_search` never raises. Engine failures, empty results, and
|
|
177
|
+
malformed input all come back as readable strings the model can react to,
|
|
178
|
+
because an exception mid-conversation kills the whole agent loop.
|
|
179
|
+
|
|
180
|
+
## Use it from Claude Code / Claude Desktop
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
pip install "webfetch-llm[all]"
|
|
184
|
+
claude mcp add webfetch webfetch-mcp
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The MCP server exposes `web_search` and `savings_report`. Run one server
|
|
188
|
+
per machine - the semantic cache assumes a single process owns its file.
|
|
189
|
+
|
|
190
|
+
## Configurations
|
|
191
|
+
|
|
192
|
+
| config | engines | search fees | when |
|
|
193
|
+
|---|---|---|---|
|
|
194
|
+
| `multi` (default for benchmarks) | DDG+Brave+Serper+Tavily fused | ~$0.012/search | best recall |
|
|
195
|
+
| `fallback` | DDG first, keyed engines catch its blocks | ~$0 typical | cheap with a safety net |
|
|
196
|
+
| `ddg` | DuckDuckGo only | $0 | no keys at all |
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from webfetch import Pipeline, SemanticSqliteCache
|
|
200
|
+
from webfetch.search import get_search_adapter
|
|
201
|
+
|
|
202
|
+
pipeline = Pipeline(search=get_search_adapter("fallback"),
|
|
203
|
+
cache=SemanticSqliteCache())
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
DDG deserves a caveat: it fingerprint-blocks automated clients with silent
|
|
207
|
+
empty responses. webfetch detects that (empty-with-peers in fusion, any
|
|
208
|
+
empty in the fallback chain), benches the engine on a circuit breaker, and
|
|
209
|
+
routes around it. The TLS side is handled by the `ddgs` dependency.
|
|
210
|
+
|
|
211
|
+
## What's inside
|
|
212
|
+
|
|
213
|
+
Search results go through: multi-engine fusion (reciprocal rank fusion
|
|
214
|
+
keyed by URL) -> concurrent fetch with a fallback chain (trafilatura ->
|
|
215
|
+
readability -> newspaper4k -> Playwright rendering for JS pages and
|
|
216
|
+
403 walls) -> 400-char chunking -> hybrid ranking (BM25 and bi-encoder
|
|
217
|
+
fused, then a cross-encoder picks the top 5) -> sentence-level compression
|
|
218
|
+
(cross-encoder scored; halves tokens with zero measured recall loss) ->
|
|
219
|
+
source-labeled output.
|
|
220
|
+
|
|
221
|
+
Results are cached at two levels (page text by URL, ranked chunks by
|
|
222
|
+
query) in a single sqlite file at `~/.webfetch/cache.db`. Cache lifetimes
|
|
223
|
+
depend on how volatile the answer is: queries are classified as
|
|
224
|
+
realtime/recent/stable (15 minutes / 7 days / 90 days), either by a hint
|
|
225
|
+
from the calling model or by a small local classifier. The model sees
|
|
226
|
+
cache provenance in every result (`[cache: semantic match to "...", 2h
|
|
227
|
+
old, recent]`) and can send `force_fresh` when it disagrees.
|
|
228
|
+
|
|
229
|
+
Everything heavy is optional. Without `[rerank]` you get BM25 ranking and
|
|
230
|
+
exact-match caching; the library degrades with a logged warning rather
|
|
231
|
+
than an ImportError.
|
|
232
|
+
|
|
233
|
+
## Cost receipts
|
|
234
|
+
|
|
235
|
+
Counters accumulate in the cache file as you use the tool:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
$ webfetch-savings
|
|
239
|
+
webfetch savings receipt (lifetime of this cache)
|
|
240
|
+
searches served: 1240
|
|
241
|
+
from cache: 472 (38%) - exact 310, semantic 162 (zero marginal cost...)
|
|
242
|
+
fresh pipeline runs: 768
|
|
243
|
+
result tokens sent: ~4,340,000 (hosted would inject ~21,576,000)
|
|
244
|
+
---
|
|
245
|
+
hosted search fees avoided: 1240 x $0.010 = $12.40
|
|
246
|
+
content-token cost avoided: ~$86.18 (at $5.00/MTok)
|
|
247
|
+
ESTIMATED TOTAL AVOIDED: $98.58
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Counters are exact; the dollar lines are estimates with the assumptions
|
|
251
|
+
(hosted fee, hosted tokens per call, your model's token price) exposed as
|
|
252
|
+
arguments to `webfetch.savings_report()`.
|
|
253
|
+
|
|
254
|
+
## Caveats, honestly
|
|
255
|
+
|
|
256
|
+
- Latency. A fresh search takes 10-40 seconds (real pages get fetched and
|
|
257
|
+
ranked locally). Hosted search returns in ~10s; Tavily-style snippet
|
|
258
|
+
APIs in ~6s. Cache hits are instant. If you need sub-second search and
|
|
259
|
+
don't care about cost, this is not your tool.
|
|
260
|
+
- The cache is single-process. Point two long-running processes at the
|
|
261
|
+
same cache file and the semantic index of one goes stale. One agent
|
|
262
|
+
loop, one MCP server, or one notebook at a time is the supported shape.
|
|
263
|
+
- Engine free tiers are real quotas. We exhausted Brave's monthly tier
|
|
264
|
+
during benchmarking; the resilience layer degraded gracefully, but your
|
|
265
|
+
recall degrades with it. Fees above are estimates from published prices.
|
|
266
|
+
- Answer quality depends on the model driving the tool. Weak models
|
|
267
|
+
formulate worse queries and re-search instead of reading (see the Haiku
|
|
268
|
+
row).
|
|
269
|
+
|
|
270
|
+
## Benchmarks and data
|
|
271
|
+
|
|
272
|
+
The eval harness has three layers: an offline matcher eval for the
|
|
273
|
+
semantic-cache thresholds, a live retrieval eval (recall, tokens, cache
|
|
274
|
+
diagnostics), and the end-to-end answer eval quoted above (SimpleQA
|
|
275
|
+
protocol: exact-match fast path, then an LLM judge). Datasets are built
|
|
276
|
+
deterministically (seeded) from SimpleQA (MIT), QQP/GLUE, and FreshQA
|
|
277
|
+
(CC-BY-SA); provenance sidecars sit next to each file in
|
|
278
|
+
[evals/datasets/](evals/datasets/). The fresh-events set was hand-written
|
|
279
|
+
against verified news sources days before the benchmark ran, specifically
|
|
280
|
+
so that no model or vendor pipeline could have seen it.
|
|
281
|
+
|
|
282
|
+
Design decisions and measured results are documented as they happened in
|
|
283
|
+
[docs/architecture.md](docs/architecture.md) and
|
|
284
|
+
[docs/ROADMAP.md](docs/ROADMAP.md).
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
MIT.
|