agent-web-search
Python client for the Agent Web Search API — self-hosted, RAG-ready web search with cosine-ranked chunks.
Returns scrubbed, chunked, cosine-ranked text from a 6-stage pipeline (SearXNG → fetch → extract → scrub → chunk → embed → rank). Drop-in for Tavily / Exa in self-hosted setups — same shape, no API keys, no usage limits, your data stays on your machine.
Why this SDK?
| agent-web-search | Tavily | Exa | |
|---|---|---|---|
| Cost | Free, self-hosted | $0.05/query | $0.005/query |
| API key | None | Required | Required |
| RAG chunk output | ✅ 256/512-token with scores | ❌ | ❌ |
| Prompt scrubbing | ✅ Built-in | ❌ | ❌ |
| JS rendering | ✅ Opt-in | ❌ | ❌ |
| Type-safe | ✅ Pydantic v2 | Partial | Partial |
| Sync + async | ✅ | ✅ | ✅ |
The agent-web-search server is the full pipeline; this client is a thin, type-safe wrapper with sync and async surfaces, Pydantic models for every response, and an exception hierarchy that maps cleanly to OpenAI's SDK pattern.
Install
pip install agent-web-search
Zero required dependencies beyond httpx and pydantic (both installed automatically).
Quick start
Sync
from agent_web_search import AgentWebSearch
with AgentWebSearch(base_url="http://localhost:8000") as client:
response = client.search("python asyncio", include_content=True)
for chunk in response.ranked_chunks:
print(f"{chunk.score:.3f} {chunk.source.title}")
print(f" {chunk.source.url}")
print(f" {chunk.text[:120]}...")
Async
import asyncio
from agent_web_search import AsyncAgentWebSearch
async def main():
async with AsyncAgentWebSearch(base_url="http://localhost:8000") as client:
response = await client.search("python asyncio")
print(response.top_chunk.text if response.top_chunk else "no results")
asyncio.run(main())
Concurrent queries
import asyncio
from agent_web_search import AsyncAgentWebSearch
async def main():
async with AsyncAgentWebSearch() as client:
results = await asyncio.gather(
client.search("python asyncio"),
client.search("rust borrow checker"),
client.search("docker healthcheck"),
)
asyncio.run(main())
API reference
AgentWebSearch(base_url=None, *, timeout=30.0, headers=None) (and async)
Create a client. All arguments are optional — sensible defaults work out of the box if the server is at http://localhost:8000.
| Argument | Type | Default | Description |
|---|---|---|---|
base_url |
str |
AGENT_WEB_SEARCH_BASE_URL env var, else http://localhost:8000 |
Server root URL |
timeout |
float |
30.0 seconds |
Per-request timeout (covers the full pipeline) |
headers |
dict[str, str] |
None |
Extra HTTP headers (e.g. future auth tokens) |
Both clients are context managers (sync: with, async: async with) and can be used without one too (just call close() / await close() when done).
client.search(query, *, render_js=False, include_content=False, categories=None, time_range=None, timeout=None)
Search the web and return cosine-ranked chunks.
| Argument | Type | Default | Description |
|---|---|---|---|
query |
str |
required | 1–1000 characters |
render_js |
bool |
False |
Use crawl4ai to render JS-heavy pages (slower; opt in only when needed) |
include_content |
bool |
False |
Fetch, extract, chunk, and rank each page. If False, ranks pre-fetched SearXNG snippets |
categories |
list[str] |
None |
Override intent routing — e.g. ["it"], ["news"], ["general"] |
time_range |
str |
None |
Time filter for news queries: "day", "week", "month", "year" |
timeout |
float |
client default | Per-call timeout override |
Returns: SearchResponse with query, ranked_chunks: list[RankedChunk], unresponsive_engines: list[str].
Raises: APITimeoutError, APIConnectionError, APIError, APIResponseError, or ValueError for invalid input.
client.health(*, timeout=None)
Ping the server. Returns HealthResponse(status="ok") on success.
Response models
SearchResponse(
query="python asyncio",
ranked_chunks=[
RankedChunk(
text="asyncio is a library...", # ~256-token chunk
parent_text="asyncio — Asynchronous I/O...", # ~512-token context
chunk_index=0,
score=0.87, # cosine similarity
source=ChunkSource(
url="https://docs.python.org/3/library/asyncio.html",
title="asyncio — Asynchronous I/O — Python 3.12 documentation",
searxng_score=1.0, # upstream popularity
),
),
...
],
unresponsive_engines=["duckduckgo"], # engines that timed out
)
SearchResponse has two convenience properties:
response.top_chunk— the highest-scoring chunk, orNoneif emptyresponse.urls— the unique source URLs, in ranking order
Configuration
| Env var | Default | Description |
|---|---|---|
AGENT_WEB_SEARCH_BASE_URL |
http://localhost:8000 |
Server URL (overridden by base_url kwarg) |
AGENT_WEB_SEARCH_TIMEOUT |
30.0 |
Default request timeout in seconds |
Errors
All exceptions inherit from AgentWebSearchError. The hierarchy mirrors the OpenAI SDK so error-handling patterns are portable:
| Exception | When it's raised | Has |
|---|---|---|
APITimeoutError |
Request timed out (client-side timeout exceeded) |
— |
APIConnectionError |
Couldn't reach the server (DNS, refused, network) | — |
APIError |
Server returned non-2xx (4xx, 5xx) | status_code, body, detail |
APIResponseError |
Server returned a body that didn't match the expected schema | — |
from agent_web_search import (
AgentWebSearch, AgentWebSearchError,
APIError, APITimeoutError, APIConnectionError,
)
client = AgentWebSearch(base_url="http://localhost:8000")
try:
response = client.search("python asyncio", timeout=15.0)
except APITimeoutError:
print("The server is too slow — try increasing timeout")
except APIConnectionError:
print("Server is not running. Start it with: docker compose up")
except APIError as e:
print(f"Server returned HTTP {e.status_code}: {e.detail}")
except AgentWebSearchError as e:
# Catch-all for any other SDK error
print(f"SDK error: {e}")
Development
git clone https://github.com/blueewhitee/agent-web-search-sdk
cd agent-web-search-sdk
# Install dev dependencies
uv sync --group dev
# Run tests
uv run pytest -v
# Type check
uv run mypy src # (if mypy is installed)
# Build sdist + wheel
uv build
# Publish to PyPI (one-time, requires API token)
export UV_PUBLISH_TOKEN="pypi-AgEIcHlwaS5vcmcC..." # token from pypi.org
uv publish
Publishing (release process)
The SDK uses PyPI Trusted Publishing via OIDC + GitHub Actions — no API tokens stored anywhere. Here's the one-time setup:
One-time setup
- Create the GitHub repo (e.g.
github.com/blueewhitee/agent-web-search). - Push the SDK code (this directory) to the repo.
- Register a Trusted Publisher on PyPI:
- Go to https://pypi.org/manage/project/agent-web-search/settings/publishing/ (or create the project first if it doesn't exist).
- Add a pending publisher (or normal publisher after the first release):
- Owner:
blueewhitee - Repository:
agent-web-search - Workflow filename:
publish.yml - Environment name:
pypi
- Owner:
- Optional first release manually (recommended, to lock in the name immediately):
# Create a PyPI API token at https://pypi.org/manage/account/token/ # Scope it to the agent-web-search project only. export UV_PUBLISH_TOKEN="pypi-..." uv build uv publish
- Switch the PyPI publisher from "pending" to "live" now that the project exists.
Every release after that
# Bump version in pyproject.toml and src/agent_web_search/__init__.py
git commit -am "release: v0.2.0"
git tag v0.2.0
git push --tags
# GitHub Actions automatically builds + publishes to PyPI via OIDC.
The workflow file at .github/workflows/publish.yml handles the rest.
Architecture
This is intentionally a thin client (~250 LOC of production code):
src/agent_web_search/client.py— syncAgentWebSearchsrc/agent_web_search/async_client.py— asyncAsyncAgentWebSearchsrc/agent_web_search/models.py— Pydantic v2 modelssrc/agent_web_search/exceptions.py— exception hierarchysrc/agent_web_search/_base.py— shared config + httpx→SDK error translation
The models are re-declared in the SDK rather than imported from the server to keep the package self-contained and the dependency surface minimal. The FastAPI server generates an OpenAPI schema at /openapi.json; a v0.2 follow-up could add codegen (datamodel-code-generator) to keep the two in sync automatically.
License
Release files for agent-web-search 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agent_web_search-0.1.0.tar.gz | 11.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agent_web_search-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.2 kB
Release files / agent_web_search-0.1.0.tar.gz
| Download URL | agent_web_search-0.1.0.tar.gz |
|---|---|
| Size | 11.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
00d85e4f6f46407a75d4edfa5e8ec9d2e2e7a041da78f36689475977b99b950f
|
|
BLAKE2b-256 checksum How to use checksums |
c1c227e30bf2e9f347f5f16ffba0f738530d45376a0e7ec211e9c3f8503e5fc6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.20 {"installer":{"name":"uv","version":"0.11.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / agent_web_search-0.1.0-py3-none-any.whl
| Download URL | agent_web_search-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
719144cb0aff7014215c5a936f54833192aa021dc05d88e9d80d2991e506a2ea
|
|
BLAKE2b-256 checksum How to use checksums |
7b6df25ee868c772911f8f6f9ec7cc950f30295760d3edad7ef80af1fcb961b3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.20 {"installer":{"name":"uv","version":"0.11.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|