search-cli (Python)
Python port of search-cli. Async library for querying multiple search providers in parallel with result deduplication.
Requirements
- Python 3.11+
Installation
pip install -e .
# With dev dependencies (pytest, respx)
pip install -e ".[dev]"
Configuration
Set API keys as environment variables. Only providers with configured keys are used.
| Variable | Provider |
|---|---|
BRAVE_API_KEY |
Brave Search |
SERPER_API_KEY |
Serper (Google) |
EXA_API_KEY |
Exa (neural search) |
JINA_API_KEY |
Jina |
TAVILY_API_KEY |
Tavily |
PERPLEXITY_API_KEY |
Perplexity |
Usage
import asyncio
from search_cli import search, Mode
async def main():
# General web search
response = await search("python asyncio tutorial")
for r in response.results:
print(r.title, r.url)
# News search
response = await search("AI news", mode=Mode.NEWS, count=10)
# Deep search (maximum coverage)
response = await search("quantum computing", mode=Mode.DEEP)
# JSON output
print(response.model_dump_json(indent=2))
asyncio.run(main())
Modes
| Mode | Providers |
|---|---|
Mode.GENERAL |
brave, serper, exa, jina, tavily, perplexity |
Mode.NEWS |
brave, serper, tavily, perplexity |
Mode.DEEP |
brave, exa, serper, tavily, perplexity |
Advanced: Manage context lifecycle
from search_cli import search, Mode, SearchContext, AppConfig
async def main():
# Reuse a single httpx client across multiple searches
async with SearchContext(config=AppConfig()) as ctx:
r1 = await search("query one", ctx=ctx)
r2 = await search("query two", mode=Mode.NEWS, ctx=ctx)
Public API
| Symbol | Type | Description |
|---|---|---|
search() |
async function |
Main entry point — query, mode, count, opts, ctx |
Mode |
Enum |
GENERAL, NEWS, DEEP |
SearchResult |
BaseModel |
title, url, snippet, source, published?, image_url?, extra? |
SearchResponse |
BaseModel |
version, status, query, mode, results, metadata |
SearchOpts |
BaseModel |
include_domains, exclude_domains, freshness |
AppConfig |
BaseSettings |
keys (ApiKeys), timeout (10s), count (5) |
ApiKeys |
BaseSettings |
6 provider API keys, loaded from env vars |
SearchContext |
class |
Async context manager holding httpx client + config |
SearchError |
Exception |
Base exception with code, message, suggestion |
Architecture
src/search_cli/
├── __init__.py # Public exports
├── enums.py # Mode enum
├── models.py # Pydantic models (SearchResult, SearchResponse, etc.)
├── config.py # ApiKeys + AppConfig (pydantic-settings, env vars)
├── context.py # SearchContext (httpx.AsyncClient lifecycle)
├── engine.py # Parallel search orchestration, URL dedup
├── errors.py # SearchError hierarchy
└── providers/
├── __init__.py # Registry: PROVIDERS_FOR_MODE, build_providers()
├── base.py # BaseProvider ABC (resolve_key, retry_request)
├── brave.py # Brave Search API
├── serper.py # Serper (Google Search) API
├── exa.py # Exa neural search API
├── jina.py # Jina search API
├── tavily.py # Tavily search API
└── perplexity.py # Perplexity chat completions API
How it works
search()receives a query + modeproviders_for_mode()selects providers mapped to that mode, filtered to those with configured API keysasyncio.TaskGrouplaunches all provider searches in parallel, each with per-provider timeout- Individual provider failures are caught and recorded (other providers continue)
- Results are deduplicated by normalized URL (first occurrence wins)
SearchResponseis returned with results + metadata (timing, provider stats)
Testing
pytest -v
23 tests covering models, engine, and all 6 providers. All tests use respx for HTTP mocking — no real API keys required.
Dependencies
| Package | Purpose |
|---|---|
httpx |
Async HTTP client (maps to Rust reqwest) |
pydantic |
Data models + JSON serialization (maps to Rust serde) |
pydantic-settings |
Config from env vars (maps to Rust figment) |
Dev only
| Package | Purpose |
|---|---|
pytest |
Test runner |
pytest-asyncio |
Async test support |
respx |
httpx request mocking |
Metadata
Release files for py-multi-agent-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 | |
|---|---|---|---|
| py_multi_agent_search-0.1.0.tar.gz | 10.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_multi_agent_search-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.5 kB
Release files / py_multi_agent_search-0.1.0.tar.gz
| Download URL | py_multi_agent_search-0.1.0.tar.gz |
|---|---|
| Size | 10.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
06cc2c4c06c8e335453c4720e421280f77199e138992fa710099c4ead45d6116
|
|
BLAKE2b-256 checksum How to use checksums |
ef150ecc004d619f5c6e9436995e258db91873f621ca90e40f986002d6a7db5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|
Release files / py_multi_agent_search-0.1.0-py3-none-any.whl
| Download URL | py_multi_agent_search-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f483e4619518682b8f62240a66acff2517c8b74fcba62c0ba6eb1a5dd8763110
|
|
BLAKE2b-256 checksum How to use checksums |
7d02350b27f5e743885c875ec2f3e938a4fb21fa45c8c265ab6a153f0f5f27dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|