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.
Files changed (79) hide show
  1. webfetch_llm-0.1.0/.claude/settings.local.json +45 -0
  2. webfetch_llm-0.1.0/.env.example +34 -0
  3. webfetch_llm-0.1.0/.github/workflows/ci.yml +38 -0
  4. webfetch_llm-0.1.0/.github/workflows/publish.yml +38 -0
  5. webfetch_llm-0.1.0/.gitignore +23 -0
  6. webfetch_llm-0.1.0/CHANGELOG.md +24 -0
  7. webfetch_llm-0.1.0/CONTRIBUTING.md +34 -0
  8. webfetch_llm-0.1.0/LICENSE +21 -0
  9. webfetch_llm-0.1.0/PKG-INFO +288 -0
  10. webfetch_llm-0.1.0/README.md +235 -0
  11. webfetch_llm-0.1.0/claude.md +57 -0
  12. webfetch_llm-0.1.0/docs/ROADMAP.md +151 -0
  13. webfetch_llm-0.1.0/docs/architecture.md +661 -0
  14. webfetch_llm-0.1.0/docs/context.md +102 -0
  15. webfetch_llm-0.1.0/evals/build_datasets.py +362 -0
  16. webfetch_llm-0.1.0/evals/common.py +147 -0
  17. webfetch_llm-0.1.0/evals/datasets/compression_chunks.jsonl +50 -0
  18. webfetch_llm-0.1.0/evals/datasets/fresh_queries_2026_07.jsonl +27 -0
  19. webfetch_llm-0.1.0/evals/datasets/fresh_queries_2026_07.meta.json +19 -0
  20. webfetch_llm-0.1.0/evals/datasets/freshqa_queries.jsonl +377 -0
  21. webfetch_llm-0.1.0/evals/datasets/freshqa_queries.meta.json +12 -0
  22. webfetch_llm-0.1.0/evals/datasets/live_queries.jsonl +65 -0
  23. webfetch_llm-0.1.0/evals/datasets/live_queries.meta.json +15 -0
  24. webfetch_llm-0.1.0/evals/datasets/matcher_pairs.jsonl +247 -0
  25. webfetch_llm-0.1.0/evals/datasets/matcher_pairs.meta.json +20 -0
  26. webfetch_llm-0.1.0/evals/fetch_datasets.py +96 -0
  27. webfetch_llm-0.1.0/evals/run_compression_eval.py +319 -0
  28. webfetch_llm-0.1.0/evals/run_e2e_eval.py +732 -0
  29. webfetch_llm-0.1.0/evals/run_matcher_eval.py +399 -0
  30. webfetch_llm-0.1.0/evals/run_pipeline_eval.py +477 -0
  31. webfetch_llm-0.1.0/evals/run_volatility_eval.py +214 -0
  32. webfetch_llm-0.1.0/evals/test_metrics.py +62 -0
  33. webfetch_llm-0.1.0/examples/agent_loop.py +115 -0
  34. webfetch_llm-0.1.0/pyproject.toml +107 -0
  35. webfetch_llm-0.1.0/requirements.txt +31 -0
  36. webfetch_llm-0.1.0/tests/test_cache.py +74 -0
  37. webfetch_llm-0.1.0/tests/test_chunker.py +36 -0
  38. webfetch_llm-0.1.0/tests/test_compress.py +101 -0
  39. webfetch_llm-0.1.0/tests/test_context.py +38 -0
  40. webfetch_llm-0.1.0/tests/test_receipts.py +28 -0
  41. webfetch_llm-0.1.0/tests/test_resilience.py +103 -0
  42. webfetch_llm-0.1.0/tests/test_tool.py +87 -0
  43. webfetch_llm-0.1.0/webfetch/__init__.py +48 -0
  44. webfetch_llm-0.1.0/webfetch/cache.py +347 -0
  45. webfetch_llm-0.1.0/webfetch/compress.py +360 -0
  46. webfetch_llm-0.1.0/webfetch/config.py +154 -0
  47. webfetch_llm-0.1.0/webfetch/data/volatility_centroids.json +1 -0
  48. webfetch_llm-0.1.0/webfetch/extract/__init__.py +35 -0
  49. webfetch_llm-0.1.0/webfetch/extract/base.py +180 -0
  50. webfetch_llm-0.1.0/webfetch/extract/claude.py +71 -0
  51. webfetch_llm-0.1.0/webfetch/extract/gemini.py +89 -0
  52. webfetch_llm-0.1.0/webfetch/extract/gpt.py +70 -0
  53. webfetch_llm-0.1.0/webfetch/extract/groq.py +83 -0
  54. webfetch_llm-0.1.0/webfetch/fetch/__init__.py +89 -0
  55. webfetch_llm-0.1.0/webfetch/fetch/base.py +42 -0
  56. webfetch_llm-0.1.0/webfetch/fetch/html.py +251 -0
  57. webfetch_llm-0.1.0/webfetch/fetch/pdf.py +246 -0
  58. webfetch_llm-0.1.0/webfetch/mcp.py +68 -0
  59. webfetch_llm-0.1.0/webfetch/pipeline.py +245 -0
  60. webfetch_llm-0.1.0/webfetch/rank/__init__.py +98 -0
  61. webfetch_llm-0.1.0/webfetch/rank/base.py +52 -0
  62. webfetch_llm-0.1.0/webfetch/rank/biencoder.py +91 -0
  63. webfetch_llm-0.1.0/webfetch/rank/bm25.py +70 -0
  64. webfetch_llm-0.1.0/webfetch/rank/chunker.py +78 -0
  65. webfetch_llm-0.1.0/webfetch/rank/crossencoder.py +73 -0
  66. webfetch_llm-0.1.0/webfetch/rank/hybrid.py +88 -0
  67. webfetch_llm-0.1.0/webfetch/rank/rrf.py +64 -0
  68. webfetch_llm-0.1.0/webfetch/receipts.py +123 -0
  69. webfetch_llm-0.1.0/webfetch/search/__init__.py +104 -0
  70. webfetch_llm-0.1.0/webfetch/search/base.py +56 -0
  71. webfetch_llm-0.1.0/webfetch/search/brave.py +91 -0
  72. webfetch_llm-0.1.0/webfetch/search/ddg.py +67 -0
  73. webfetch_llm-0.1.0/webfetch/search/multi.py +145 -0
  74. webfetch_llm-0.1.0/webfetch/search/resilience.py +153 -0
  75. webfetch_llm-0.1.0/webfetch/search/serper.py +90 -0
  76. webfetch_llm-0.1.0/webfetch/search/tavily.py +74 -0
  77. webfetch_llm-0.1.0/webfetch/semcache.py +270 -0
  78. webfetch_llm-0.1.0/webfetch/tool.py +190 -0
  79. 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.