Relying on a single search provider means your agent fails when that provider has downtime, rate limits, or poor coverage for a specific query type. This tutorial builds a multi-engine search agent that cascades through providers (Scavio, Brave, SearXNG) and selects the best result set. The agent tries the cheapest provider first and falls back to alternatives only when needed, keeping costs minimal.
Prerequisites
- Python 3.9+ installed
- requests library installed
- A Scavio API key from scavio.dev
- Optional: Brave API key for fallback
Walkthrough
Step 1: Define the search engine cascade
Configure multiple search providers in priority order. Each has a search function, cost, and quality threshold for accepting results.
import requests, os, time
SCAVIO_KEY = os.environ.get('SCAVIO_API_KEY', '')
BRAVE_KEY = os.environ.get('BRAVE_API_KEY', '')
def search_scavio(query: str, count: int = 10) -> list:
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
json={'query': query, 'country_code': 'us', 'num_results': count},
timeout=10)
if resp.status_code != 200:
return []
return [{'title': r['title'], 'link': r['link'], 'snippet': r.get('snippet', '')}
for r in resp.json().get('organic_results', [])]
def search_brave(query: str, count: int = 10) -> list:
if not BRAVE_KEY:
return []
resp = requests.get('https://api.search.brave.com/res/v1/web/search',
headers={'X-Subscription-Token': BRAVE_KEY},
params={'q': query, 'count': count}, timeout=10)
if resp.status_code != 200:
return []
return [{'title': r['title'], 'link': r['url'], 'snippet': r.get('description', '')}
for r in resp.json().get('web', {}).get('results', [])]
ENGINES = [
{'name': 'scavio', 'fn': search_scavio, 'cost': 0.005, 'min_results': 3},
{'name': 'brave', 'fn': search_brave, 'cost': 0.005, 'min_results': 3},
]
print(f'Configured {len(ENGINES)} search engines in cascade')
for e in ENGINES:
print(f' {e["name"]}: ${e["cost"]}/query, min {e["min_results"]} results')Step 2: Build the cascading search function
Try each engine in order. Accept results if they meet the quality threshold. Track which engine succeeded and total cost.
def cascade_search(query: str, count: int = 10) -> dict:
"""Search with cascading fallback across engines."""
attempts = []
for engine in ENGINES:
start = time.time()
try:
results = engine['fn'](query, count)
latency = int((time.time() - start) * 1000)
attempts.append({
'engine': engine['name'], 'results': len(results),
'latency_ms': latency, 'cost': engine['cost']
})
if len(results) >= engine['min_results']:
total_cost = sum(a['cost'] for a in attempts)
return {
'results': results,
'engine': engine['name'],
'attempts': attempts,
'total_cost': total_cost
}
except Exception as e:
attempts.append({
'engine': engine['name'], 'error': str(e),
'cost': engine['cost']
})
# All engines failed or returned poor results
total_cost = sum(a['cost'] for a in attempts)
best = max(attempts, key=lambda a: a.get('results', 0))
return {
'results': [],
'engine': 'none',
'attempts': attempts,
'total_cost': total_cost,
'note': 'All engines returned insufficient results'
}
result = cascade_search('best python web framework 2026')
print(f'Engine: {result["engine"]}')
print(f'Results: {len(result["results"])}')
print(f'Cost: ${result["total_cost"]:.3f}')
for a in result['attempts']:
print(f' {a["engine"]}: {a.get("results", "error")} results, {a.get("latency_ms", "N/A")}ms')Step 3: Add smart engine selection based on query type
Route different query types to the best engine. Technical queries might do better on one engine while news queries work better on another.
def smart_search(query: str, count: int = 10) -> dict:
"""Route query to best engine based on query type."""
query_lower = query.lower()
# Classify query type
if any(w in query_lower for w in ['news', 'latest', 'today', 'just', 'announced']):
query_type = 'news'
elif any(w in query_lower for w in ['site:', 'filetype:', 'inurl:']):
query_type = 'advanced'
elif any(w in query_lower for w in ['how to', 'tutorial', 'guide', 'example']):
query_type = 'tutorial'
else:
query_type = 'general'
# Reorder engines based on query type
# For most queries, default cascade works fine
# For advanced queries, Scavio handles operators better
result = cascade_search(query, count)
result['query_type'] = query_type
return result
# Test different query types
test_queries = [
'latest AI news today',
'site:github.com python search api',
'how to build a search agent',
]
for q in test_queries:
r = smart_search(q)
print(f'[{r["query_type"]}] {q[:40]} -> {r["engine"]} ({len(r["results"])} results, ${r["total_cost"]:.3f})')Python Example
import requests, os, time
SCAVIO_KEY = os.environ.get('SCAVIO_API_KEY', '')
BRAVE_KEY = os.environ.get('BRAVE_API_KEY', '')
def search(query, count=10):
# Try Scavio first
try:
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
json={'query': query, 'country_code': 'us', 'num_results': count}, timeout=10)
results = resp.json().get('organic_results', [])
if len(results) >= 3:
return {'results': results, 'engine': 'scavio', 'cost': 0.005}
except Exception:
pass
# Fallback to Brave
if BRAVE_KEY:
try:
resp = requests.get('https://api.search.brave.com/res/v1/web/search',
headers={'X-Subscription-Token': BRAVE_KEY},
params={'q': query, 'count': count}, timeout=10)
results = resp.json().get('web', {}).get('results', [])
return {'results': results, 'engine': 'brave', 'cost': 0.010}
except Exception:
pass
return {'results': [], 'engine': 'none', 'cost': 0}
r = search('python web framework 2026')
print(f'{r["engine"]}: {len(r["results"])} results, ${r["cost"]}')JavaScript Example
const SCAVIO_KEY = process.env.SCAVIO_API_KEY;
const BRAVE_KEY = process.env.BRAVE_API_KEY;
async function search(query, count = 10) {
try {
const resp = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, country_code: 'us', num_results: count })
});
const results = (await resp.json()).organic_results || [];
if (results.length >= 3) return { results, engine: 'scavio', cost: 0.005 };
} catch {}
if (BRAVE_KEY) {
try {
const resp = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`, {
headers: { 'X-Subscription-Token': BRAVE_KEY }
});
const results = (await resp.json()).web?.results || [];
return { results, engine: 'brave', cost: 0.01 };
} catch {}
}
return { results: [], engine: 'none', cost: 0 };
}
search('python framework 2026').then(r => console.log(`${r.engine}: ${r.results.length} results`));Expected Output
Configured 2 search engines in cascade
scavio: $0.005/query, min 3 results
brave: $0.005/query, min 3 results
Engine: scavio
Results: 10
Cost: $0.005
scavio: 10 results, 245ms
[news] latest AI news today -> scavio (10 results, $0.005)
[advanced] site:github.com python search api -> scavio (8 results, $0.005)
[tutorial] how to build a search agent -> scavio (10 results, $0.005)