
Web Search Plus
- 88 installs
- 638 repo stars
- Updated March 7, 2026
- sundial-org/awesome-openclaw-skills
Helps with ai & agent building tasks during AI-assisted development.
About
web-search-plus is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- web-search-plus
- AI & Agent Building
- AI-coding skill
Web Search Plus by the numbers
- 88 all-time installs (skills.sh)
- Ranked #4,935 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sundial-org/awesome-openclaw-skills --skill web-search-plusAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 88 |
|---|---|
| repo stars | ★ 638 |
| Last updated | March 7, 2026 |
| Repository | sundial-org/awesome-openclaw-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Web Search Plus
Multi-provider web search with Intelligent Auto-Routing: Serper (Google), Tavily (Research), Exa (Neural).
NEW in v2.3.0: Interactive setup wizard! Run python3 scripts/setup.py for guided configuration.
NEW in v2.2.5: Automatic error fallback — if one provider fails (rate limit, timeout, etc.), automatically tries the next provider in priority order!
---
🚀 First Run (Setup Wizard)
New to web-search-plus? The interactive setup wizard guides you through configuration:
python3 scripts/setup.pyThe wizard will: 1. Explain each provider — What they're best for, free tier limits, signup links 2. Ask which providers to enable — You can use 1, 2, or all 3 3. Collect API keys — Keys are stored locally in config.json (gitignored) 4. Configure defaults — Default provider, auto-routing, result count
What Each Provider Is Best For
| Provider | Best For | Free Tier |
|---|---|---|
| Serper | Google results, shopping, prices, local businesses, news | 2,500/month |
| Tavily | Research questions, explanations, academic, full-page content | 1,000/month |
| Exa | Semantic search, "similar to X", startup discovery, papers | 1,000/month |
Reconfigure Anytime
python3 scripts/setup.py --reset---
🔑 API Keys Setup (Manual)
NEW in v2.2.0: The script auto-loads API keys from .env in the skill directory!
Quick Setup
Option A: .env file (recommended)
# /path/to/skills/web-search-plus/.env
export SERPER_API_KEY="your-key" # https://serper.dev
export TAVILY_API_KEY="your-key" # https://tavily.com
export EXA_API_KEY="your-key" # https://exa.aiOption B: config.json (NEW in v2.2.1)
# Copy the example config
cp config.example.json config.jsonThen add your keys:
{
"serper": { "api_key": "your-serper-key" },
"tavily": { "api_key": "your-tavily-key" },
"exa": { "api_key": "your-exa-key" }
}⚠️ config.json is gitignored — your keys stay safe!
Just run — keys load automatically:
python3 scripts/search.py -q "your query"
# No need for 'source .env' anymore! ✨Priority: config.json > .env > environment variable
Get Free API Keys
| Provider | Free Tier | Sign Up |
|---|---|---|
| Serper | 2,500 queries/mo | https://serper.dev |
| Tavily | 1,000 queries/mo | https://tavily.com |
| Exa | 1,000 queries/mo | https://exa.ai |
---
⚠️ Don't Modify Core OpenClaw Config
Tavily, Serper, and Exa are NOT core OpenClaw providers.
❌ DON'T add to ~/.openclaw/openclaw.json:
"tools": { "web": { "search": { "provider": "tavily" }}} // WRONG!✅ DO use this skill's scripts — keys auto-load from .env
Core OpenClaw only supports brave as the built-in web search provider. This skill adds Serper, Tavily, and Exa as additional options via its own scripts.
---
🧠 Intelligent Auto-Routing
No need to choose a provider — just search! The skill uses multi-signal analysis to understand your query intent:
# These queries are intelligently routed with confidence scoring:
python3 scripts/search.py -q "how much does iPhone 16 cost" # → Serper (68% MEDIUM)
python3 scripts/search.py -q "how does quantum entanglement work" # → Tavily (86% HIGH)
python3 scripts/search.py -q "startups similar to Notion" # → Exa (76% HIGH)
python3 scripts/search.py -q "MacBook Pro M3 specs review" # → Serper (70% HIGH)
python3 scripts/search.py -q "explain pros and cons of React" # → Tavily (85% HIGH)
python3 scripts/search.py -q "companies like stripe.com" # → Exa (100% HIGH)How It Works
The routing engine analyzes multiple signals:
🛒 Shopping Intent → Serper
| Signal Type | Examples | Weight |
|---|---|---|
| Price patterns | "how much", "price of", "cost of" | HIGH |
| Purchase intent | "buy", "purchase", "order", "where to buy" | HIGH |
| Deal signals | "deal", "discount", "cheap", "best price" | MEDIUM |
| Product + Brand | "iPhone 16", "Sony headphones" + specs/review | HIGH |
| Local business | "near me", "restaurants", "hotels" | HIGH |
📚 Research Intent → Tavily
| Signal Type | Examples | Weight |
|---|---|---|
| Explanation | "how does", "why does", "explain", "what is" | HIGH |
| Analysis | "compare", "pros and cons", "difference between" | HIGH |
| Learning | "tutorial", "guide", "understand", "learn" | MEDIUM |
| Depth | "in-depth", "comprehensive", "detailed" | MEDIUM |
| Complex queries | Long, multi-clause questions | BONUS |
🔍 Discovery Intent → Exa
| Signal Type | Examples | Weight |
|---|---|---|
| Similarity | "similar to", "alternatives to", "competitors" | VERY HIGH |
| Company discovery | "companies like", "startups doing", "who else" | HIGH |
| URL detection | Any URL or domain (stripe.com) | VERY HIGH |
| Academic | "arxiv", "research papers", "github projects" | HIGH |
| Funding | "Series A", "YC", "funded startup" | HIGH |
Confidence Scoring
Every routing decision includes a confidence level:
| Confidence | Level | Meaning |
|---|---|---|
| 70-100% | HIGH | Strong signal match, very reliable |
| 40-69% | MEDIUM | Good match, should work well |
| 0-39% | LOW | Ambiguous query, using fallback |
Debug Routing Decisions
See the full analysis:
python3 scripts/search.py --explain-routing -q "how much does iPhone 16 Pro cost"Output:
{
"query": "how much does iPhone 16 Pro cost",
"routing_decision": {
"provider": "serper",
"confidence": 0.68,
"confidence_level": "medium",
"reason": "moderate_confidence_match"
},
"scores": {"serper": 7.0, "tavily": 0.0, "exa": 0.0},
"top_signals": [
{"matched": "how much", "weight": 4.0},
{"matched": "brand + product detected", "weight": 3.0}
],
"query_analysis": {
"word_count": 7,
"is_complex": false,
"has_url": null,
"recency_focused": false
}
}---
🔍 When to Use This Skill vs Built-in Brave Search
Use Built-in Brave Search when:
- ✅ General web searches (news, info, questions)
- ✅ Privacy is important
- ✅ Quick lookups without specific requirements
Use web-search-plus when:
→ Serper (Google results):
- 🛍️ Product specs, prices, shopping - "Compare iPhone 16 vs Samsung S24"
- 📍 Local businesses, places - "Best pizza in Berlin"
- 🎯 "Google it" - Explicitly wants Google results
- 📰 Shopping/images/news -
--type shopping/images/news - 🏆 Knowledge Graph - Structured info (prices, ratings, etc.)
→ Tavily (AI-optimized research):
- 📚 Research questions - "How does quantum computing work?"
- 🔬 Deep dives - Complex multi-part questions
- 📄 Full page content - Not just snippets (
--raw-content) - 🎓 Academic research - Synthesized answers
- 🔒 Domain filtering -
--include-domainsfor trusted sources
→ Exa (Neural semantic search):
- 🔗 Similar pages - "Sites like OpenAI.com" (
--similar-url) - 🏢 Company discovery - "AI companies like Anthropic"
- 📝 Research papers -
--category "research paper" - 💻 GitHub projects -
--category github - 📅 Date-specific -
--start-date/--end-date
---
Provider Comparison
| Feature | Serper | Tavily | Exa |
|---|---|---|---|
| Speed | ⚡⚡⚡ | ⚡⚡ | ⚡⚡ |
| Factual Accuracy | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Semantic Understanding | ⭐ | ⭐⭐ | ⭐⭐⭐ |
| Research Quality | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Full Page Content | ✗ | ✓ | ✓ |
| Shopping/Local | ✓ | ✗ | ✗ |
| Similar Pages | ✗ | ✗ | ✓ |
| Knowledge Graph | ✓ | ✗ | ✗ |
---
Usage Examples
Auto-Routed (Recommended)
python3 scripts/search.py -q "iPhone 16 Pro Max price" # → Serper
python3 scripts/search.py -q "how does HTTPS encryption work" # → Tavily
python3 scripts/search.py -q "startups similar to Notion" # → ExaExplicit Provider
python3 scripts/search.py -p serper -q "weather Berlin" --type weather
python3 scripts/search.py -p tavily -q "quantum computing" --depth advanced
python3 scripts/search.py -p exa --similar-url "https://stripe.com" --category company---
Configuration
config.json
{
"auto_routing": {
"enabled": true,
"fallback_provider": "serper",
"confidence_threshold": 0.3,
"disabled_providers": []
},
"serper": {"country": "us", "language": "en"},
"tavily": {"depth": "advanced"},
"exa": {"type": "neural"}
}---
Output Format
{
"provider": "serper",
"query": "iPhone 16 price",
"results": [{"title": "...", "url": "...", "snippet": "...", "score": 0.95}],
"answer": "Synthesized answer...",
"routing": {
"auto_routed": true,
"provider": "serper",
"confidence": 0.78,
"confidence_level": "high",
"reason": "high_confidence_match",
"top_signals": [{"matched": "price", "weight": 3.0}]
}
}---
FAQ
General
Q: How does auto-routing decide which provider to use?
Multi-signal analysis scores each provider based on: price patterns, explanation phrases, similarity keywords, URLs, product+brand combos, and query complexity. Highest score wins. Use --explain-routing to see the decision breakdown.Q: What if it picks the wrong provider?
Override with-p serper/tavily/exa. Check--explain-routingto understand why it chose differently.
Q: What does "low confidence" mean?
Query is ambiguous (e.g., "Tesla" could be cars, stock, or company). Falls back to Serper. Results may vary.
Q: Can I disable a provider?
Yes! In config.json: "disabled_providers": ["exa"]API Keys
Q: Which API keys do I need?
At minimum ONE key. You can use just Serper, just Tavily, or all three. Missing keys = that provider is skipped.
Q: Where do I get API keys?
- Serper: https://serper.dev (2,500 free queries, no credit card)
- Tavily: https://tavily.com (1,000 free searches/month)
- Exa: https://exa.ai (1,000 free searches/month)
Q: How do I set API keys?
Two options (both auto-load):
>
Option A: .env file
```bash
export SERPER_API_KEY="your-key"
```
>
Option B: config.json (v2.2.1+)
```json
{ "serper": { "api_key": "your-key" } }
```
Routing Details
Q: How do I know which provider handled my search?
Checkrouting.providerin JSON output, or[🔍 Searched with: Provider]in chat responses.
Q: Why does it sometimes choose Serper for research questions?
If the query has brand/product signals (e.g., "how does Tesla FSD work"), shopping intent may outweigh research intent. Override with -p tavily.Q: What's the confidence threshold?
Default: 0.3 (30%). Below this = low confidence, uses fallback. Adjustable in config.json.
Troubleshooting
Q: "No API key found" error?
1. Check.envexists in skill folder withexport VAR=valueformat
2. Keys auto-load from skill's .env since v2.2.03. Or set in system environment: export SERPER_API_KEY="..."Q: Getting empty results?
1. Check API key is valid
2. Try a different provider with -p3. Some queries have no results (very niche topics)
Q: Rate limited?
NEW in v2.2.5: Automatic fallback! If one provider hits rate limits, the script automatically tries the next provider in priority order (serper → tavily → exa). You'll see fallback info in stderr and the response will include routing.fallback_used: true.>
Provider limits: Serper 2,500 free total, Tavily 1,000/month free, Exa 1,000/month free.
For OpenClaw Users
Q: How do I use this in chat?
Just ask! OpenClaw auto-detects search intent. Or explicitly: "search with web-search-plus for..."
Q: Does it replace built-in Brave Search?
No, it's complementary. Use Brave for quick lookups, web-search-plus for research/shopping/discovery.
Q: Can I see which provider was used?
Yes! SOUL.md can include attribution: [🔍 Searched with: Serper/Tavily/Exa]Changelog - Web Search Plus
[2.1.5] - 2026-01-27
📝 Documentation
- Added warning about NOT using Tavily/Serper/Exa in core OpenClaw config
- Core OpenClaw only supports
braveas the built-in provider - This skill's providers must be used via environment variables and scripts, not
openclaw.json
[2.1.0] - 2026-01-23
🧠 Intelligent Multi-Signal Routing
Completely overhauled auto-routing with sophisticated query analysis:
Intent Classification
- Shopping Intent: Detects price patterns ("how much", "cost of"), purchase signals ("buy", "order"), deal keywords, and product+brand combinations
- Research Intent: Identifies explanation patterns ("how does", "why does"), analysis signals ("pros and cons", "compare"), learning keywords, and complex multi-clause queries
- Discovery Intent: Recognizes similarity patterns ("similar to", "alternatives"), company discovery signals, URL/domain detection, and academic patterns
Linguistic Pattern Detection
- "How much" / "price of" → Shopping (Serper)
- "How does" / "Why does" / "Explain" → Research (Tavily)
- "Companies like" / "Similar to" / "Alternatives" → Discovery (Exa)
- Product + Brand name combos → Shopping (Serper)
- URLs and domains in query → Similar search (Exa)
Query Analysis Features
- Complexity scoring: Long, multi-clause queries get routed to research providers
- URL detection: Automatic detection of URLs/domains triggers Exa similar search
- Brand recognition: Tech brands (Apple, Samsung, Sony, etc.) with product terms → shopping
- Recency signals: "latest", "2026", "breaking" boost news mode
Confidence Scoring
- HIGH (70-100%): Strong signal match, very reliable routing
- MEDIUM (40-69%): Good match, should work well
- LOW (0-39%): Ambiguous query, using fallback provider
- Confidence based on absolute signal strength + relative margin over alternatives
Enhanced Debug Mode
python3 scripts/search.py --explain-routing -q "your query"Now shows:
- Routing decision with confidence level
- All provider scores
- Top matched signals with weights
- Query analysis (complexity, URL detection, recency focus)
- All matched patterns per provider
🔧 Technical Changes
QueryAnalyzer Class
New QueryAnalyzer class with:
SHOPPING_SIGNALS: 25+ weighted patterns for shopping intentRESEARCH_SIGNALS: 30+ weighted patterns for research intentDISCOVERY_SIGNALS: 20+ weighted patterns for discovery intentLOCAL_NEWS_SIGNALS: 25+ patterns for local/news queriesBRAND_PATTERNS: Tech brand detection regex
Signal Weighting
- Multi-word phrases get higher weights (e.g., "how much" = 4.0 vs "price" = 3.0)
- Strong signals: price patterns (4.0), similarity patterns (5.0), URLs (5.0)
- Medium signals: product terms (2.5), learning keywords (2.5)
- Bonus scoring: Product+brand combo (+3.0), complex query (+2.5)
Improved Output Format
{
"routing": {
"auto_routed": true,
"provider": "serper",
"confidence": 0.78,
"confidence_level": "high",
"reason": "high_confidence_match",
"top_signals": [{"matched": "price", "weight": 3.0}],
"scores": {"serper": 7.0, "tavily": 0.0, "exa": 0.0}
}
}📚 Documentation Updates
- SKILL.md: Complete rewrite with signal tables and confidence scoring guide
- README.md: Updated with intelligent routing examples and confidence levels
- FAQ: Updated to explain multi-signal analysis
🧪 Test Results
| Query | Provider | Confidence | Signals |
|---|---|---|---|
| "how much does iPhone 16 cost" | Serper | 68% | "how much", brand+product |
| "how does quantum entanglement work" | Tavily | 86% HIGH | "how does", "what are", "implications" |
| "startups similar to Notion" | Exa | 76% HIGH | "similar to", "Series A" |
| "companies like stripe.com" | Exa | 100% HIGH | URL detected, "companies like" |
| "MacBook Pro M3 specs review" | Serper | 70% HIGH | brand+product, "specs", "review" |
| "Tesla" | Serper | 0% LOW | No signals (fallback) |
| "arxiv papers on transformers" | Exa | 58% | "arxiv" |
| "latest AI news 2026" | Serper | 77% HIGH | "latest", "news", "2026" |
---
[2.0.0] - 2026-01-23
🎉 Major Features
Smart Auto-Routing
- Automatic provider selection based on query analysis
- No need to manually choose provider - just search!
- Intelligent keyword matching for routing decisions
- Pattern detection for query types (shopping, research, discovery)
- Scoring system for provider selection
User Configuration
- config.json: Full control over auto-routing behavior
- Configurable keyword mappings: Add your own routing keywords
- Provider priority: Set tie-breaker order
- Disable providers: Turn off providers you don't have API keys for
- Enable/disable auto-routing: Opt-in or opt-out as needed
Debugging Tools
- --explain-routing flag: See exactly why a provider was selected
- Detailed routing metadata in JSON responses
- Shows matched keywords and routing scores
📚 Documentation
- README.md: Complete auto-routing guide with examples
- SKILL.md: Detailed routing logic and configuration reference
- FAQ section: Common questions about auto-routing
- Configuration examples: Pre-built configs for common use cases
---
[1.0.x] - Initial Release
- Multi-provider search: Serper, Tavily, Exa
- Manual provider selection with
-pflag - Unified JSON output format
- Provider-specific options (--depth, --category, --similar-url, etc.)
- Domain filtering for Tavily/Exa
- Date filtering for Exa
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$comment": "Web Search Plus configuration — intelligent routing and provider settings",
"defaults": {
"provider": "serper",
"max_results": 5
},
"auto_routing": {
"enabled": true,
"fallback_provider": "serper",
"provider_priority": [
"serper",
"tavily",
"exa"
],
"disabled_providers": [],
"confidence_threshold": 0.3,
"keyword_mappings": {
"serper": [
"price",
"buy",
"shop",
"shopping",
"cost",
"deal",
"sale",
"purchase",
"cheap",
"expensive",
"store",
"product",
"review",
"specs",
"specification",
"where to buy",
"near me",
"local",
"restaurant",
"hotel",
"weather",
"news",
"latest",
"breaking",
"map",
"directions",
"phone number",
"preis",
"kaufen",
"bestellen",
"günstig",
"billig",
"teuer",
"kosten",
"angebot",
"rabatt",
"shop",
"händler",
"geschäft",
"laden",
"test",
"bewertung",
"technische daten",
"spezifikationen",
"wo kaufen",
"in der nähe",
"wetter",
"nachrichten",
"aktuell",
"neu"
],
"tavily": [
"how does",
"how to",
"explain",
"research",
"what is",
"why does",
"analyze",
"compare",
"study",
"academic",
"detailed",
"comprehensive",
"in-depth",
"understand",
"learn",
"tutorial",
"guide",
"overview",
"history of",
"background",
"context",
"implications",
"pros and cons",
"wie funktioniert",
"erklärung",
"erklären",
"was ist",
"warum",
"analyse",
"vergleich",
"vergleichen",
"studie",
"verstehen",
"lernen",
"anleitung",
"tutorial",
"überblick",
"hintergrund",
"vor- und nachteile"
],
"exa": [
"similar to",
"companies like",
"find sites like",
"alternatives to",
"competitors",
"startup",
"github",
"paper",
"research paper",
"arxiv",
"pdf",
"academic paper",
"similar pages",
"related sites",
"who else",
"other companies",
"comparable to",
"ähnlich wie",
"firmen wie",
"alternativen zu",
"konkurrenten",
"vergleichbar mit",
"andere unternehmen"
]
}
},
"serper": {
"country": "us",
"language": "en",
"type": "search",
"autocorrect": true,
"include_images": false
},
"tavily": {
"depth": "advanced",
"topic": "general",
"max_results": 8
},
"exa": {
"type": "neural",
"category": null,
"include_domains": [],
"exclude_domains": []
}
}
{
"name": "@openclaw/web-search-plus",
"version": "2.3.0",
"description": "Unified search skill with Intelligent Auto-Routing. Uses multi-signal analysis (intent classification, linguistic patterns, URL/brand detection) to automatically select between Serper (Google), Tavily (Research), and Exa (Neural) with confidence scoring.",
"keywords": [
"openclaw",
"skill",
"search",
"web-search",
"serper",
"tavily",
"exa",
"google-search",
"research",
"semantic-search",
"ai-agent",
"auto-routing",
"smart-routing",
"multi-provider",
"shopping",
"product-search",
"similar-sites",
"company-discovery",
"free-tier",
"api-aggregator"
],
"author": "robbyczgw-cla",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/robbyczgw-cla/web-search-plus.git"
},
"homepage": "https://clawhub.ai/robbyczgw-cla/web-search-plus",
"bugs": {
"url": "https://github.com/robbyczgw-cla/web-search-plus/issues"
},
"openclaw": {
"skill": true,
"triggers": [
"search",
"find",
"look up",
"research"
],
"capabilities": [
"web-search",
"image-search",
"semantic-search",
"multi-provider"
],
"providers": [
"serper",
"tavily",
"exa"
]
},
"files": [
"SKILL.md",
"README.md",
"scripts/",
".env.example"
]
}
Web Search Plus
Unified multi-provider web search with Intelligent Auto-Routing — uses multi-signal analysis to automatically select between Serper, Tavily, and Exa with confidence scoring.
  
---
🧠 Features (v2.3.0)
Intelligent Multi-Signal Routing — The skill now uses sophisticated query analysis:
- Intent Classification: Shopping vs Research vs Discovery
- Linguistic Patterns: "how much" (price) vs "how does" (research)
- Entity Detection: Product+brand combos, URLs, domains
- Complexity Analysis: Long queries favor research providers
- Confidence Scoring: Know how reliable the routing decision is
python3 scripts/search.py -q "how much does iPhone 16 cost" # → Serper (68% confidence)
python3 scripts/search.py -q "how does quantum entanglement work" # → Tavily (86% HIGH)
python3 scripts/search.py -q "startups similar to Notion" # → Exa (76% HIGH)
python3 scripts/search.py -q "companies like stripe.com" # → Exa (100% HIGH - URL detected)---
🔍 When to Use Which Provider
Built-in Brave Search (OpenClaw default)
- ✅ General web searches
- ✅ Privacy-focused
- ✅ Quick lookups
- ✅ Default fallback
Serper (Google Results)
- 🛍️ Product specs, prices, shopping
- 📍 Local businesses, places
- 🎯 "Google it" - explicit Google results
- 📰 Shopping/images needed
- 🏆 Knowledge Graph data
Tavily (AI-Optimized Research)
- 📚 Research questions, deep dives
- 🔬 Complex multi-part queries
- 📄 Need full page content (not just snippets)
- 🎓 Academic/technical research
- 🔒 Domain filtering (trusted sources)
Exa (Neural Semantic Search)
- 🔗 Find similar pages
- 🏢 Company/startup discovery
- 📝 Research papers
- 💻 GitHub projects
- 📅 Date-specific content
---
Table of Contents
- Quick Start
- Smart Auto-Routing
- Configuration Guide
- Provider Deep Dives
- Usage Examples
- Workflow Examples
- Optimization Tips
- FAQ & Troubleshooting
- API Reference
---
Quick Start
Option A: Interactive Setup (Recommended)
# Run the setup wizard - it guides you through everything
python3 scripts/setup.pyThe wizard explains each provider, collects your API keys, and creates config.json automatically.
Option B: Manual Setup
# 1. Set up at least one API key
export SERPER_API_KEY="your-key" # https://serper.dev
export TAVILY_API_KEY="your-key" # https://tavily.com
export EXA_API_KEY="your-key" # https://exa.ai
# 2. Run a search (auto-routed!)
python3 scripts/search.py -q "best laptop 2024"Run a Search
# Auto-routed to best provider
python3 scripts/search.py -q "best laptop 2024"
# Or specify a provider explicitly
python3 scripts/search.py -p serper -q "iPhone 16 specs"
python3 scripts/search.py -p tavily -q "quantum computing explained" --depth advanced
python3 scripts/search.py -p exa -q "AI startups 2024" --category company---
Smart Auto-Routing
How It Works
When you don't specify a provider, the skill analyzes your query and routes it to the best provider:
| Query Contains | Routes To | Example |
|---|---|---|
| "price", "buy", "shop", "cost" | Serper | "iPhone 16 price" |
| "near me", "restaurant", "hotel" | Serper | "pizza near me" |
| "weather", "news", "latest" | Serper | "weather Berlin" |
| "how does", "explain", "what is" | Tavily | "how does TCP work" |
| "research", "study", "analyze" | Tavily | "climate research" |
| "tutorial", "guide", "learn" | Tavily | "python tutorial" |
| "similar to", "companies like" | Exa | "companies like Stripe" |
| "startup", "Series A" | Exa | "AI startups Series A" |
| "github", "research paper" | Exa | "LLM papers arxiv" |
Examples
# These are all auto-routed to the optimal provider:
python3 scripts/search.py -q "MacBook Pro M3 price" # → Serper
python3 scripts/search.py -q "how does HTTPS work" # → Tavily
python3 scripts/search.py -q "startups like Notion" # → Exa
python3 scripts/search.py -q "best sushi restaurant near me" # → Serper
python3 scripts/search.py -q "explain attention mechanism" # → Tavily
python3 scripts/search.py -q "alternatives to Figma" # → ExaDebug Auto-Routing
See exactly why a provider was selected:
python3 scripts/search.py --explain-routing -q "best laptop to buy"Output:
{
"query": "best laptop to buy",
"selected_provider": "serper",
"reason": "matched_keywords (score=2)",
"matched_keywords": ["buy", "best"],
"available_providers": ["serper", "tavily", "exa"]
}Routing Info in Results
Every search result includes routing information:
{
"provider": "serper",
"query": "iPhone 16 price",
"results": [...],
"routing": {
"auto_routed": true,
"selected_provider": "serper",
"reason": "matched_keywords (score=1)",
"matched_keywords": ["price"]
}
}---
Configuration Guide
Environment Variables
Create a .env file or set these in your shell:
# Required: Set at least one
export SERPER_API_KEY="your-serper-key"
export TAVILY_API_KEY="your-tavily-key"
export EXA_API_KEY="your-exa-key"Config File (config.json)
The config.json file lets you customize auto-routing and provider defaults:
{
"defaults": {
"provider": "serper",
"max_results": 5
},
"auto_routing": {
"enabled": true,
"fallback_provider": "serper",
"provider_priority": ["serper", "tavily", "exa"],
"disabled_providers": [],
"keyword_mappings": {
"serper": ["price", "buy", "shop", "cost", "deal", "near me", "weather"],
"tavily": ["how does", "explain", "research", "what is", "tutorial"],
"exa": ["similar to", "companies like", "alternatives", "startup", "github"]
}
},
"serper": {
"country": "us",
"language": "en"
},
"tavily": {
"depth": "basic",
"topic": "general"
},
"exa": {
"type": "neural"
}
}Configuration Examples
Example 1: Disable Exa (Only Use Serper + Tavily)
{
"auto_routing": {
"disabled_providers": ["exa"]
}
}Example 2: Make Tavily the Default
{
"auto_routing": {
"fallback_provider": "tavily"
}
}Example 3: Add Custom Keywords
{
"auto_routing": {
"keyword_mappings": {
"serper": [
"price", "buy", "shop", "amazon", "ebay", "walmart",
"deal", "discount", "coupon", "sale", "cheap"
],
"tavily": [
"how does", "explain", "research", "what is",
"coursera", "udemy", "learn", "course", "certification"
],
"exa": [
"similar to", "companies like", "competitors",
"YC company", "funded startup", "Series A", "Series B"
]
}
}
}Example 4: German Locale for Serper
{
"serper": {
"country": "de",
"language": "de"
}
}Example 5: Disable Auto-Routing
{
"auto_routing": {
"enabled": false
},
"defaults": {
"provider": "serper"
}
}Example 6: Research-Heavy Config
{
"auto_routing": {
"fallback_provider": "tavily",
"provider_priority": ["tavily", "serper", "exa"]
},
"tavily": {
"depth": "advanced",
"include_raw_content": true
}
}---
Provider Deep Dives
Serper (Google Search API)
What it is: Direct access to Google Search results via API — the same results you'd see on google.com.
Strengths
| Strength | Description |
|---|---|
| 🎯 Accuracy | Google's search quality, knowledge graph, featured snippets |
| 🛒 Shopping | Product prices, reviews, shopping results |
| 📍 Local | Business listings, maps, places |
| 📰 News | Real-time news with Google News integration |
| 🖼️ Images | Google Images search |
| ⚡ Speed | Fastest response times (~200-400ms) |
Best Use Cases
- ✅ Product specifications and comparisons
- ✅ Shopping and price lookups
- ✅ Local business searches ("restaurants near me")
- ✅ Quick factual queries (weather, conversions, definitions)
- ✅ News headlines and current events
- ✅ Image searches
- ✅ When you need "what Google shows"
Getting Your API Key
1. Go to serper.dev 2. Sign up with email or Google 3. Copy your API key from the dashboard 4. Set SERPER_API_KEY environment variable
---
Tavily (Research Search)
What it is: AI-optimized search engine built for research and RAG applications — returns synthesized answers plus full content.
Strengths
| Strength | Description |
|---|---|
| 📚 Research Quality | Optimized for comprehensive, accurate research |
| 💬 AI Answers | Returns synthesized answers, not just links |
| 📄 Full Content | Can return complete page content (raw_content) |
| 🎯 Domain Filtering | Include/exclude specific domains |
| 🔬 Deep Mode | Advanced search for thorough research |
| 📰 Topic Modes | Specialized for general vs news content |
Best Use Cases
- ✅ Research questions requiring synthesized answers
- ✅ Academic or technical deep dives
- ✅ When you need actual page content (not just snippets)
- ✅ Multi-source information comparison
- ✅ Domain-specific research (filter to authoritative sources)
- ✅ News research with context
- ✅ RAG/LLM applications
Getting Your API Key
1. Go to tavily.com 2. Sign up and verify email 3. Navigate to API Keys section 4. Generate and copy your key 5. Set TAVILY_API_KEY environment variable
---
Exa (Neural Search)
What it is: Neural/semantic search engine that understands meaning, not just keywords — finds conceptually similar content.
Strengths
| Strength | Description |
|---|---|
| 🧠 Semantic Understanding | Finds results by meaning, not keywords |
| 🔗 Similar Pages | Find pages similar to a reference URL |
| 🏢 Company Discovery | Excellent for finding startups, companies |
| 📑 Category Filters | Filter by type (company, paper, tweet, etc.) |
| 📅 Date Filtering | Precise date range searches |
| 🎓 Academic | Great for research papers and technical content |
Best Use Cases
- ✅ Conceptual queries ("companies building X")
- ✅ Finding similar companies or pages
- ✅ Startup and company discovery
- ✅ Research paper discovery
- ✅ Finding GitHub projects
- ✅ Date-filtered searches for recent content
- ✅ When keyword matching fails
Getting Your API Key
1. Go to exa.ai 2. Sign up with email or Google 3. Navigate to API section in dashboard 4. Copy your API key 5. Set EXA_API_KEY environment variable
---
Usage Examples
Auto-Routed Searches (Recommended)
# Just search — the skill picks the best provider
python3 scripts/search.py -q "Tesla Model 3 price"
python3 scripts/search.py -q "how do neural networks learn"
python3 scripts/search.py -q "YC startups like Stripe"Serper Options
# Different search types
python3 scripts/search.py -p serper -q "gaming monitor" --type shopping
python3 scripts/search.py -p serper -q "coffee shop" --type places
python3 scripts/search.py -p serper -q "AI news" --type news
# With time filter
python3 scripts/search.py -p serper -q "OpenAI news" --time-range day
# Include images
python3 scripts/search.py -p serper -q "iPhone 16 Pro" --images
# Different locale
python3 scripts/search.py -p serper -q "Wetter Wien" --country at --language deTavily Options
# Deep research mode
python3 scripts/search.py -p tavily -q "quantum computing applications" --depth advanced
# With full page content
python3 scripts/search.py -p tavily -q "transformer architecture" --raw-content
# Domain filtering
python3 scripts/search.py -p tavily -q "AI research" --include-domains arxiv.org nature.comExa Options
# Category filtering
python3 scripts/search.py -p exa -q "AI startups Series A" --category company
python3 scripts/search.py -p exa -q "attention mechanism" --category "research paper"
# Date filtering
python3 scripts/search.py -p exa -q "YC companies" --start-date 2024-01-01
# Find similar pages
python3 scripts/search.py -p exa --similar-url "https://stripe.com" --category company---
Workflow Examples
🛒 Product Research Workflow
# Step 1: Get product specs (auto-routed to Serper)
python3 scripts/search.py -q "MacBook Pro M3 Max specs"
# Step 2: Check prices (auto-routed to Serper)
python3 scripts/search.py -q "MacBook Pro M3 Max price comparison"
# Step 3: In-depth reviews (auto-routed to Tavily)
python3 scripts/search.py -q "detailed MacBook Pro M3 Max review"📚 Academic Research Workflow
# Step 1: Understand the topic (auto-routed to Tavily)
python3 scripts/search.py -q "explain transformer architecture in deep learning"
# Step 2: Find recent papers (Exa)
python3 scripts/search.py -p exa -q "transformer improvements" --category "research paper" --start-date 2024-01-01
# Step 3: Find implementations (Exa)
python3 scripts/search.py -p exa -q "transformer implementation" --category github🏢 Competitive Analysis Workflow
# Step 1: Find competitors (auto-routed to Exa)
python3 scripts/search.py -q "companies like Notion"
# Step 2: Find similar products (Exa)
python3 scripts/search.py -p exa --similar-url "https://notion.so" --category company
# Step 3: Deep dive comparison (Tavily)
python3 scripts/search.py -p tavily -q "Notion vs Coda comparison" --depth advanced---
Optimization Tips
Cost Optimization
| Tip | Savings |
|---|---|
| Use auto-routing (defaults to Serper, cheapest) | Best value |
Use Tavily basic before advanced | ~50% cost reduction |
Set appropriate max_results | Linear cost savings |
| Use Exa only for semantic queries | Avoid waste |
Performance Optimization
| Tip | Impact |
|---|---|
| Serper is fastest (~200ms) | Use for time-sensitive queries |
Tavily basic faster than advanced | ~2x faster |
Lower max_results = faster response | Linear improvement |
---
FAQ & Troubleshooting
General Questions
Q: Do I need API keys for all three providers?
No. You only need keys for providers you want to use. Auto-routing skips providers without keys.
Q: Which provider should I start with?
Serper — it's the fastest, cheapest, and has the largest free tier (2,500 queries).
Q: Can I use multiple providers in one workflow?
Yes! That's the recommended approach. See Workflow Examples.
Q: How do I reduce API costs?
Use auto-routing (defaults to cheapest), start with lowermax_results, use Tavilybasicbeforeadvanced.
Auto-Routing Questions
Q: Why did my query go to the wrong provider?
Use --explain-routing to debug. Add custom keywords to config.json if needed.Q: Can I add my own keywords?
Yes! Editconfig.json→auto_routing.keyword_mappings.
Q: How does keyword scoring work?
Multi-word phrases get higher weights. "companies like" (2 words) scores higher than "like" (1 word).
Q: What if no keywords match?
Uses the fallback provider (default: Serper).
Q: Can I force a specific provider?
Yes, use-p serper,-p tavily, or-p exa.
Troubleshooting
Error: "Missing API key"
# Check if key is set
echo $SERPER_API_KEY
# Set it
export SERPER_API_KEY="your-key"Error: "API Error (401)"
Your API key is invalid or expired. Generate a new one.
Error: "API Error (429)"
Rate limited. Wait and retry, or upgrade your plan.
Empty results?
Try a different provider, broaden your query, or remove restrictive filters.
Slow responses?
Reducemax_results, use Tavilybasic, or use Serper (fastest).
---
API Reference
Output Format
All providers return unified JSON:
{
"provider": "serper|tavily|exa",
"query": "original search query",
"results": [
{
"title": "Page Title",
"url": "https://example.com/page",
"snippet": "Content excerpt...",
"score": 0.95,
"date": "2024-01-15",
"raw_content": "Full page content (Tavily only)"
}
],
"images": ["url1", "url2"],
"answer": "Synthesized answer",
"knowledge_graph": { },
"routing": {
"auto_routed": true,
"selected_provider": "serper",
"reason": "matched_keywords (score=1)",
"matched_keywords": ["price"]
}
}CLI Options Reference
| Option | Providers | Description |
|---|---|---|
-q, --query | All | Search query |
-p, --provider | All | Provider: auto, serper, tavily, exa |
-n, --max-results | All | Max results (default: 5) |
--auto | All | Force auto-routing |
--explain-routing | All | Debug auto-routing |
--images | Serper, Tavily | Include images |
--country | Serper | Country code (default: us) |
--language | Serper | Language code (default: en) |
--type | Serper | search/news/images/videos/places/shopping |
--time-range | Serper | hour/day/week/month/year |
--depth | Tavily | basic/advanced |
--topic | Tavily | general/news |
--raw-content | Tavily | Include full page content |
--exa-type | Exa | neural/keyword |
--category | Exa | company/research paper/news/pdf/github/tweet |
--start-date | Exa | Start date (YYYY-MM-DD) |
--end-date | Exa | End date (YYYY-MM-DD) |
--similar-url | Exa | Find similar pages |
--include-domains | Tavily, Exa | Only these domains |
--exclude-domains | Tavily, Exa | Exclude these domains |
--compact | All | Compact JSON output |
---
License
MIT
---
Links
#!/usr/bin/env python3
"""
Web Search Plus — Unified Multi-Provider Search with Intelligent Auto-Routing
Supports: Serper (Google), Tavily (Research), Exa (Neural)
Smart Routing uses multi-signal analysis:
- Query intent classification (shopping, research, discovery)
- Linguistic pattern detection (how much vs how does)
- Product/brand recognition
- URL detection
- Confidence scoring
Usage:
python3 search.py --query "..." # Auto-route based on query
python3 search.py --provider [serper|tavily|exa] --query "..." [options]
Examples:
python3 search.py -q "iPhone 16 Pro price" # → Serper (shopping intent)
python3 search.py -q "how does quantum entanglement work" # → Tavily (research intent)
python3 search.py -q "startups similar to Notion" # → Exa (discovery intent)
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
# =============================================================================
# Auto-load .env from skill directory (if exists)
# =============================================================================
def _load_env_file():
"""Load .env file from skill root directory if it exists."""
env_path = Path(__file__).parent.parent / ".env"
if env_path.exists():
with open(env_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
# Handle export VAR=value or VAR=value
if line.startswith("export "):
line = line[7:]
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
_load_env_file()
# =============================================================================
# Configuration
# =============================================================================
DEFAULT_CONFIG = {
"defaults": {
"provider": "serper",
"max_results": 5
},
"auto_routing": {
"enabled": True,
"fallback_provider": "serper",
"provider_priority": ["serper", "tavily", "exa"],
"disabled_providers": [],
"confidence_threshold": 0.3, # Below this, note low confidence
},
"serper": {
"country": "us",
"language": "en",
"type": "search"
},
"tavily": {
"depth": "basic",
"topic": "general"
},
"exa": {
"type": "neural"
}
}
def load_config() -> Dict[str, Any]:
"""Load configuration from config.json if it exists, with defaults."""
config = DEFAULT_CONFIG.copy()
config_path = Path(__file__).parent.parent / "config.json"
if config_path.exists():
try:
with open(config_path) as f:
user_config = json.load(f)
for key, value in user_config.items():
if isinstance(value, dict) and key in config:
config[key] = {**config.get(key, {}), **value}
else:
config[key] = value
except (json.JSONDecodeError, IOError) as e:
print(json.dumps({
"warning": f"Could not load config.json: {e}",
"using": "default configuration"
}), file=sys.stderr)
return config
def get_api_key(provider: str, config: Dict[str, Any] = None) -> Optional[str]:
"""Get API key for provider from config.json or environment.
Priority: config.json > .env > environment variable
"""
# Check config.json first
if config:
provider_config = config.get(provider, {})
if isinstance(provider_config, dict):
key = provider_config.get("api_key") or provider_config.get("apiKey")
if key:
return key
# Then check environment
key_map = {
"serper": "SERPER_API_KEY",
"tavily": "TAVILY_API_KEY",
"exa": "EXA_API_KEY",
}
return os.environ.get(key_map.get(provider, ""))
# Backward compatibility alias
def get_env_key(provider: str) -> Optional[str]:
"""Get API key for provider from environment (legacy function)."""
return get_api_key(provider)
def validate_api_key(provider: str, config: Dict[str, Any] = None) -> str:
"""Validate and return API key, with helpful error messages."""
key = get_api_key(provider, config)
if not key:
env_var = {
"serper": "SERPER_API_KEY",
"tavily": "TAVILY_API_KEY",
"exa": "EXA_API_KEY"
}[provider]
urls = {
"serper": "https://serper.dev",
"tavily": "https://tavily.com",
"exa": "https://exa.ai"
}
error_msg = {
"error": f"Missing API key for {provider}",
"env_var": env_var,
"how_to_fix": [
f"1. Get your API key from {urls[provider]}",
f"2. Add to config.json: \"{provider}\": {{\"api_key\": \"your-key\"}}",
f"3. Or set environment variable: export {env_var}=\"your-key\"",
],
"provider": provider
}
print(json.dumps(error_msg, indent=2), file=sys.stderr)
sys.exit(1)
if len(key) < 10:
print(json.dumps({
"error": f"API key for {provider} appears invalid (too short)",
"provider": provider
}, indent=2), file=sys.stderr)
sys.exit(1)
return key
# =============================================================================
# Intelligent Auto-Routing Engine
# =============================================================================
class QueryAnalyzer:
"""
Intelligent query analysis for smart provider routing.
Uses multi-signal analysis:
- Intent classification (shopping, research, discovery, local, news)
- Linguistic patterns (question structure, phrase patterns)
- Entity detection (products, brands, URLs, dates)
- Complexity assessment
"""
# Intent signal patterns with weights
# Higher weight = stronger signal for that provider
SHOPPING_SIGNALS = {
# Price patterns (very strong)
r'\bhow much\b': 4.0,
r'\bprice of\b': 4.0,
r'\bcost of\b': 4.0,
r'\bprices?\b': 3.0,
r'\$\d+|\d+\s*dollars?': 3.0,
r'€\d+|\d+\s*euros?': 3.0,
r'£\d+|\d+\s*pounds?': 3.0,
# German price patterns (sehr stark)
r'\bpreis(e)?\b': 3.5,
r'\bkosten\b': 3.0,
r'\bwieviel\b': 3.5,
r'\bwie viel\b': 3.5,
r'\bwas kostet\b': 4.0,
# Purchase intent (strong)
r'\bbuy\b': 3.5,
r'\bpurchase\b': 3.5,
r'\border\b(?!\s+by)': 3.0, # "order" but not "order by"
r'\bshopping\b': 3.5,
r'\bshop for\b': 3.5,
r'\bwhere to (buy|get|purchase)\b': 4.0,
# German purchase intent (stark)
r'\bkaufen\b': 3.5,
r'\bbestellen\b': 3.5,
r'\bwo kaufen\b': 4.0,
r'\bhändler\b': 3.0,
r'\bshop\b': 2.5,
# Deal/discount signals
r'\bdeal(s)?\b': 3.0,
r'\bdiscount(s)?\b': 3.0,
r'\bsale\b': 2.5,
r'\bcheap(er|est)?\b': 3.0,
r'\baffordable\b': 2.5,
r'\bbudget\b': 2.5,
r'\bbest price\b': 3.5,
r'\bcompare prices\b': 3.5,
r'\bcoupon\b': 3.0,
# German deal/discount signals
r'\bgünstig(er|ste)?\b': 3.0,
r'\bbillig(er|ste)?\b': 3.0,
r'\bangebot(e)?\b': 3.0,
r'\brabatt\b': 3.0,
r'\baktion\b': 2.5,
r'\bschnäppchen\b': 3.0,
# Product comparison
r'\bvs\.?\b': 2.0,
r'\bversus\b': 2.0,
r'\bor\b.*\bwhich\b': 2.0,
r'\bspecs?\b': 2.5,
r'\bspecifications?\b': 2.5,
r'\breview(s)?\b': 2.0,
r'\brating(s)?\b': 2.0,
r'\bunboxing\b': 2.5,
# German product comparison
r'\btest\b': 2.5,
r'\bbewertung(en)?\b': 2.5,
r'\btechnische daten\b': 3.0,
r'\bspezifikationen\b': 2.5,
}
RESEARCH_SIGNALS = {
# Explanation patterns (very strong)
r'\bhow does\b': 4.0,
r'\bhow do\b': 3.5,
r'\bwhy does\b': 4.0,
r'\bwhy do\b': 3.5,
r'\bwhy is\b': 3.5,
r'\bexplain\b': 4.0,
r'\bexplanation\b': 4.0,
r'\bwhat is\b': 3.0,
r'\bwhat are\b': 3.0,
r'\bdefine\b': 3.5,
r'\bdefinition of\b': 3.5,
r'\bmeaning of\b': 3.0,
# Analysis patterns (strong)
r'\banalyze\b': 3.5,
r'\banalysis\b': 3.5,
r'\bcompare\b(?!\s*prices?)': 3.0, # compare but not "compare prices"
r'\bcomparison\b': 3.0,
r'\bpros and cons\b': 4.0,
r'\badvantages?\b': 3.0,
r'\bdisadvantages?\b': 3.0,
r'\bbenefits?\b': 2.5,
r'\bdrawbacks?\b': 3.0,
r'\bdifference between\b': 3.5,
# Learning patterns
r'\bunderstand\b': 3.0,
r'\blearn(ing)?\b': 2.5,
r'\btutorial\b': 3.0,
r'\bguide\b': 2.5,
r'\bhow to\b': 2.0, # Lower weight - could be shopping too
r'\bstep by step\b': 3.0,
# Depth signals
r'\bin[- ]depth\b': 3.0,
r'\bdetailed\b': 2.5,
r'\bcomprehensive\b': 3.0,
r'\bthorough\b': 2.5,
r'\bdeep dive\b': 3.5,
r'\boverall\b': 2.0,
r'\bsummary\b': 2.0,
# Academic patterns
r'\bstudy\b': 2.5,
r'\bresearch shows\b': 3.5,
r'\baccording to\b': 2.5,
r'\bevidence\b': 3.0,
r'\bscientific\b': 3.0,
r'\bhistory of\b': 3.0,
r'\bbackground\b': 2.5,
r'\bcontext\b': 2.5,
r'\bimplications?\b': 3.0,
# German explanation patterns (sehr stark)
r'\bwie funktioniert\b': 4.0,
r'\bwarum\b': 3.5,
r'\berklär(en|ung)?\b': 4.0,
r'\bwas ist\b': 3.0,
r'\bwas sind\b': 3.0,
r'\bbedeutung\b': 3.0,
# German analysis patterns
r'\banalyse\b': 3.5,
r'\bvergleich(en)?\b': 3.0,
r'\bvor- und nachteile\b': 4.0,
r'\bvorteile\b': 3.0,
r'\bnachteile\b': 3.0,
r'\bunterschied(e)?\b': 3.5,
# German learning patterns
r'\bverstehen\b': 3.0,
r'\blernen\b': 2.5,
r'\banleitung\b': 3.0,
r'\bübersicht\b': 2.5,
r'\bhintergrund\b': 2.5,
r'\bzusammenfassung\b': 2.5,
}
DISCOVERY_SIGNALS = {
# Similarity patterns (very strong)
r'\bsimilar to\b': 5.0,
r'\blike\s+\w+\.com': 4.5, # "like notion.com"
r'\balternatives? to\b': 5.0,
r'\bcompetitors? (of|to)\b': 4.5,
r'\bcompeting with\b': 4.0,
r'\brivals? (of|to)\b': 4.0,
r'\binstead of\b': 3.0,
r'\breplacement for\b': 3.5,
# Company/startup patterns (strong)
r'\bcompanies (like|that|doing|building)\b': 4.5,
r'\bstartups? (like|that|doing|building)\b': 4.5,
r'\bwho else\b': 4.0,
r'\bother (companies|startups|tools|apps)\b': 3.5,
r'\bfind (companies|startups|tools)\b': 4.0,
# Funding/business patterns
r'\bseries [a-d]\b': 4.0,
r'\byc\b|y combinator': 4.0,
r'\bfund(ed|ing|raise)\b': 3.5,
r'\bventure\b': 3.0,
r'\bvaluation\b': 3.0,
# Category patterns
r'\bresearch papers? (on|about)\b': 4.0,
r'\barxiv\b': 4.5,
r'\bgithub (projects?|repos?)\b': 4.5,
r'\bopen source\b.*\bprojects?\b': 4.0,
r'\btweets? (about|on)\b': 3.5,
r'\bblogs? (about|on|like)\b': 3.0,
# URL detection (very strong signal for Exa similar)
r'https?://[^\s]+': 5.0,
r'\b\w+\.(com|org|io|ai|co|dev)\b': 3.5,
}
LOCAL_NEWS_SIGNALS = {
# Local patterns → Serper
r'\bnear me\b': 4.0,
r'\bnearby\b': 3.5,
r'\blocal\b': 3.0,
r'\bin (my )?(city|area|town|neighborhood)\b': 3.5,
r'\brestaurants?\b': 2.5,
r'\bhotels?\b': 2.5,
r'\bcafes?\b': 2.5,
r'\bstores?\b': 2.0,
r'\bdirections? to\b': 3.5,
r'\bmap of\b': 3.0,
r'\bphone number\b': 3.0,
r'\baddress of\b': 3.0,
r'\bopen(ing)? hours\b': 3.0,
# Weather/time
r'\bweather\b': 4.0,
r'\bforecast\b': 3.5,
r'\btemperature\b': 3.0,
r'\btime in\b': 3.0,
# News/recency patterns → Serper (or Tavily for news depth)
r'\blatest\b': 2.5,
r'\brecent\b': 2.5,
r'\btoday\b': 2.5,
r'\bbreaking\b': 3.5,
r'\bnews\b': 2.5,
r'\bheadlines?\b': 3.0,
r'\b202[4-9]\b': 2.0, # Current year mentions
r'\blast (week|month|year)\b': 2.0,
}
# Brand/product patterns for shopping detection
BRAND_PATTERNS = [
# Tech brands
r'\b(apple|iphone|ipad|macbook|airpods?)\b',
r'\b(samsung|galaxy)\b',
r'\b(google|pixel)\b',
r'\b(microsoft|surface|xbox)\b',
r'\b(sony|playstation)\b',
r'\b(nvidia|geforce|rtx)\b',
r'\b(amd|ryzen|radeon)\b',
r'\b(intel|core i[3579])\b',
r'\b(dell|hp|lenovo|asus|acer)\b',
r'\b(lg|tcl|hisense)\b',
# Product categories
r'\b(laptop|phone|tablet|tv|monitor|headphones?|earbuds?)\b',
r'\b(camera|lens|drone)\b',
r'\b(watch|smartwatch|fitbit|garmin)\b',
r'\b(router|modem|wifi)\b',
r'\b(keyboard|mouse|gaming)\b',
]
def __init__(self, config: Dict[str, Any]):
self.config = config
self.auto_config = config.get("auto_routing", DEFAULT_CONFIG["auto_routing"])
def _calculate_signal_score(
self,
query: str,
signals: Dict[str, float]
) -> Tuple[float, List[Dict[str, Any]]]:
"""
Calculate score for a signal category.
Returns (total_score, list of matched signals with details).
"""
query_lower = query.lower()
matches = []
total_score = 0.0
for pattern, weight in signals.items():
regex = re.compile(pattern, re.IGNORECASE)
found = regex.findall(query_lower)
if found:
# Normalize found matches
match_text = found[0] if isinstance(found[0], str) else found[0][0] if found[0] else pattern
matches.append({
"pattern": pattern,
"matched": match_text,
"weight": weight
})
total_score += weight
return total_score, matches
def _detect_product_brand_combo(self, query: str) -> float:
"""
Detect product + brand combinations which strongly indicate shopping intent.
Returns a bonus score.
"""
query_lower = query.lower()
brand_found = False
product_found = False
for pattern in self.BRAND_PATTERNS:
if re.search(pattern, query_lower, re.IGNORECASE):
brand_found = True
break
# Check for product indicators
product_indicators = [
r'\b(buy|price|specs?|review|vs|compare)\b',
r'\b(pro|max|plus|mini|ultra|lite)\b', # Product tier names
r'\b\d+\s*(gb|tb|inch|mm|hz)\b', # Specifications
]
for pattern in product_indicators:
if re.search(pattern, query_lower, re.IGNORECASE):
product_found = True
break
if brand_found and product_found:
return 3.0 # Strong shopping signal
elif brand_found:
return 1.5 # Moderate shopping signal
return 0.0
def _detect_url(self, query: str) -> Optional[str]:
"""Detect URLs in query - strong signal for Exa similar search."""
url_pattern = r'https?://[^\s]+'
match = re.search(url_pattern, query)
if match:
return match.group()
# Also check for domain-like patterns
domain_pattern = r'\b(\w+\.(com|org|io|ai|co|dev|net|app))\b'
match = re.search(domain_pattern, query, re.IGNORECASE)
if match:
return match.group()
return None
def _assess_query_complexity(self, query: str) -> Dict[str, Any]:
"""
Assess query complexity - complex queries favor Tavily.
"""
words = query.split()
word_count = len(words)
# Count question words
question_words = len(re.findall(
r'\b(what|why|how|when|where|which|who|whose|whom)\b',
query, re.IGNORECASE
))
# Check for multiple clauses
clause_markers = len(re.findall(
r'\b(and|but|or|because|since|while|although|if|when)\b',
query, re.IGNORECASE
))
complexity_score = 0.0
if word_count > 10:
complexity_score += 1.5
if word_count > 20:
complexity_score += 1.0
if question_words > 1:
complexity_score += 1.0
if clause_markers > 0:
complexity_score += 0.5 * clause_markers
return {
"word_count": word_count,
"question_words": question_words,
"clause_markers": clause_markers,
"complexity_score": complexity_score,
"is_complex": complexity_score > 2.0
}
def _detect_recency_intent(self, query: str) -> Tuple[bool, float]:
"""
Detect if query wants recent/timely information.
Returns (is_recency_focused, score).
"""
recency_patterns = [
(r'\b(latest|newest|recent|current)\b', 2.5),
(r'\b(today|yesterday|this week|this month)\b', 3.0),
(r'\b(202[4-9]|2030)\b', 2.0),
(r'\b(breaking|live|just|now)\b', 3.0),
(r'\blast (hour|day|week|month)\b', 2.5),
]
total = 0.0
for pattern, weight in recency_patterns:
if re.search(pattern, query, re.IGNORECASE):
total += weight
return total > 2.0, total
def analyze(self, query: str) -> Dict[str, Any]:
"""
Perform comprehensive query analysis.
Returns detailed analysis with scores for each provider.
"""
# Calculate scores for each intent category
shopping_score, shopping_matches = self._calculate_signal_score(
query, self.SHOPPING_SIGNALS
)
research_score, research_matches = self._calculate_signal_score(
query, self.RESEARCH_SIGNALS
)
discovery_score, discovery_matches = self._calculate_signal_score(
query, self.DISCOVERY_SIGNALS
)
local_news_score, local_news_matches = self._calculate_signal_score(
query, self.LOCAL_NEWS_SIGNALS
)
# Apply product/brand bonus to shopping
brand_bonus = self._detect_product_brand_combo(query)
if brand_bonus > 0:
shopping_score += brand_bonus
shopping_matches.append({
"pattern": "product_brand_combo",
"matched": "brand + product detected",
"weight": brand_bonus
})
# Detect URL → strong Exa signal
detected_url = self._detect_url(query)
if detected_url:
discovery_score += 5.0
discovery_matches.append({
"pattern": "url_detected",
"matched": detected_url,
"weight": 5.0
})
# Assess complexity → favors Tavily
complexity = self._assess_query_complexity(query)
if complexity["is_complex"]:
research_score += complexity["complexity_score"]
research_matches.append({
"pattern": "query_complexity",
"matched": f"complex query ({complexity['word_count']} words)",
"weight": complexity["complexity_score"]
})
# Check recency intent
is_recency, recency_score = self._detect_recency_intent(query)
# Map intents to providers with final scores
provider_scores = {
"serper": shopping_score + local_news_score + (recency_score * 0.5),
"tavily": research_score + (complexity["complexity_score"] if not complexity["is_complex"] else 0),
"exa": discovery_score,
}
# Build match details per provider
provider_matches = {
"serper": shopping_matches + local_news_matches,
"tavily": research_matches,
"exa": discovery_matches,
}
return {
"query": query,
"provider_scores": provider_scores,
"provider_matches": provider_matches,
"detected_url": detected_url,
"complexity": complexity,
"recency_focused": is_recency,
"recency_score": recency_score,
}
def route(self, query: str) -> Dict[str, Any]:
"""
Route query to optimal provider with confidence scoring.
"""
analysis = self.analyze(query)
scores = analysis["provider_scores"]
# Filter to available providers
disabled = set(self.auto_config.get("disabled_providers", []))
available = {
p: s for p, s in scores.items()
if p not in disabled and get_env_key(p)
}
if not available:
# No providers available, use fallback
fallback = self.auto_config.get("fallback_provider", "serper")
return {
"provider": fallback,
"confidence": 0.0,
"confidence_level": "low",
"reason": "no_available_providers",
"scores": scores,
"top_signals": [],
"analysis": analysis,
}
# Find the winner
max_score = max(available.values())
total_score = sum(available.values()) or 1.0
# Handle ties using priority
priority = self.auto_config.get("provider_priority", ["serper", "tavily", "exa"])
winners = [p for p, s in available.items() if s == max_score]
if len(winners) > 1:
# Use priority to break tie
for p in priority:
if p in winners:
winner = p
break
else:
winner = winners[0]
else:
winner = winners[0]
# Calculate confidence
# High confidence = clear winner with good margin
if max_score == 0:
confidence = 0.0
reason = "no_signals_matched"
else:
# Confidence based on:
# 1. Absolute score (is it strong enough?)
# 2. Relative margin (is there a clear winner?)
second_best = sorted(available.values(), reverse=True)[1] if len(available) > 1 else 0
margin = (max_score - second_best) / max_score if max_score > 0 else 0
# Normalize score to 0-1 range (assuming max reasonable score ~15)
normalized_score = min(max_score / 15.0, 1.0)
# Confidence is combination of absolute strength and relative margin
confidence = round((normalized_score * 0.6 + margin * 0.4), 3)
if confidence >= 0.7:
reason = "high_confidence_match"
elif confidence >= 0.4:
reason = "moderate_confidence_match"
else:
reason = "low_confidence_match"
# Get top signals for the winning provider
matches = analysis["provider_matches"].get(winner, [])
top_signals = sorted(matches, key=lambda x: x["weight"], reverse=True)[:5]
# Special case: URL detected and Exa available → strong recommendation
if analysis["detected_url"] and "exa" in available:
if winner != "exa":
# Override if URL is present but didn't win
# (user might want similar search)
pass # Keep current winner but note it
# Build detailed routing result
threshold = self.auto_config.get("confidence_threshold", 0.3)
return {
"provider": winner,
"confidence": confidence,
"confidence_level": "high" if confidence >= 0.7 else "medium" if confidence >= 0.4 else "low",
"reason": reason,
"scores": {p: round(s, 2) for p, s in available.items()},
"winning_score": round(max_score, 2),
"top_signals": [
{"matched": s["matched"], "weight": s["weight"]}
for s in top_signals
],
"below_threshold": confidence < threshold,
"analysis_summary": {
"query_length": len(query.split()),
"is_complex": analysis["complexity"]["is_complex"],
"has_url": analysis["detected_url"] is not None,
"recency_focused": analysis["recency_focused"],
}
}
def auto_route_provider(query: str, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Intelligently route query to the best provider.
Returns detailed routing decision with confidence.
"""
analyzer = QueryAnalyzer(config)
return analyzer.route(query)
def explain_routing(query: str, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Provide detailed explanation of routing decision for debugging.
"""
analyzer = QueryAnalyzer(config)
analysis = analyzer.analyze(query)
routing = analyzer.route(query)
return {
"query": query,
"routing_decision": {
"provider": routing["provider"],
"confidence": routing["confidence"],
"confidence_level": routing["confidence_level"],
"reason": routing["reason"],
},
"scores": routing["scores"],
"top_signals": routing["top_signals"],
"intent_breakdown": {
"shopping_signals": len(analysis["provider_matches"]["serper"]),
"research_signals": len(analysis["provider_matches"]["tavily"]),
"discovery_signals": len(analysis["provider_matches"]["exa"]),
},
"query_analysis": {
"word_count": analysis["complexity"]["word_count"],
"is_complex": analysis["complexity"]["is_complex"],
"complexity_score": round(analysis["complexity"]["complexity_score"], 2),
"has_url": analysis["detected_url"],
"recency_focused": analysis["recency_focused"],
},
"all_matches": {
provider: [
{"matched": m["matched"], "weight": m["weight"]}
for m in matches
]
for provider, matches in analysis["provider_matches"].items()
if matches
},
"available_providers": [
p for p in ["serper", "tavily", "exa"]
if get_env_key(p) and p not in config.get("auto_routing", {}).get("disabled_providers", [])
]
}
# =============================================================================
# HTTP Client
# =============================================================================
def make_request(url: str, headers: dict, body: dict, timeout: int = 30) -> dict:
"""Make HTTP POST request and return JSON response."""
# Ensure User-Agent is set (required by some APIs like Exa/Cloudflare)
if "User-Agent" not in headers:
headers["User-Agent"] = "ClawdBot-WebSearchPlus/2.1"
data = json.dumps(body).encode("utf-8")
req = Request(url, data=data, headers=headers, method="POST")
try:
with urlopen(req, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as e:
error_body = e.read().decode("utf-8") if e.fp else str(e)
try:
error_json = json.loads(error_body)
error_detail = error_json.get("error") or error_json.get("message") or error_body
except json.JSONDecodeError:
error_detail = error_body[:500]
error_messages = {
401: "Invalid or expired API key. Please check your credentials.",
403: "Access forbidden. Your API key may not have permission for this operation.",
429: "Rate limit exceeded. Please wait a moment and try again.",
500: "Server error. The search provider is experiencing issues.",
503: "Service unavailable. The search provider may be down."
}
friendly_msg = error_messages.get(e.code, f"API error: {error_detail}")
raise Exception(f"{friendly_msg} (HTTP {e.code})")
except URLError as e:
raise Exception(f"Network error: {e.reason}. Check your internet connection.")
except TimeoutError:
raise Exception(f"Request timed out after {timeout}s. Try again or reduce max_results.")
# =============================================================================
# Serper (Google Search API)
# =============================================================================
def search_serper(
query: str,
api_key: str,
max_results: int = 5,
country: str = "us",
language: str = "en",
search_type: str = "search",
time_range: Optional[str] = None,
include_images: bool = False,
) -> dict:
"""Search using Serper (Google Search API)."""
endpoint = f"https://google.serper.dev/{search_type}"
body = {
"q": query,
"gl": country,
"hl": language,
"num": max_results,
"autocorrect": True,
}
if time_range and time_range != "none":
tbs_map = {
"hour": "qdr:h",
"day": "qdr:d",
"week": "qdr:w",
"month": "qdr:m",
"year": "qdr:y",
}
if time_range in tbs_map:
body["tbs"] = tbs_map[time_range]
headers = {
"X-API-KEY": api_key,
"Content-Type": "application/json",
}
data = make_request(endpoint, headers, body)
results = []
for i, item in enumerate(data.get("organic", [])[:max_results]):
results.append({
"title": item.get("title", ""),
"url": item.get("link", ""),
"snippet": item.get("snippet", ""),
"score": round(1.0 - i * 0.1, 2),
"date": item.get("date"),
})
answer = ""
if data.get("answerBox", {}).get("answer"):
answer = data["answerBox"]["answer"]
elif data.get("answerBox", {}).get("snippet"):
answer = data["answerBox"]["snippet"]
elif data.get("knowledgeGraph", {}).get("description"):
answer = data["knowledgeGraph"]["description"]
elif results:
answer = results[0]["snippet"]
images = []
if include_images:
try:
img_data = make_request(
"https://google.serper.dev/images",
headers,
{"q": query, "gl": country, "hl": language, "num": 5},
)
images = [img.get("imageUrl", "") for img in img_data.get("images", [])[:5] if img.get("imageUrl")]
except Exception:
pass
return {
"provider": "serper",
"query": query,
"results": results,
"images": images,
"answer": answer,
"knowledge_graph": data.get("knowledgeGraph"),
"related_searches": [r.get("query") for r in data.get("relatedSearches", [])]
}
# =============================================================================
# Tavily (Research Search)
# =============================================================================
def search_tavily(
query: str,
api_key: str,
max_results: int = 5,
depth: str = "basic",
topic: str = "general",
include_domains: Optional[List[str]] = None,
exclude_domains: Optional[List[str]] = None,
include_images: bool = False,
include_raw_content: bool = False,
) -> dict:
"""Search using Tavily (AI Research Search)."""
endpoint = "https://api.tavily.com/search"
body = {
"api_key": api_key,
"query": query,
"max_results": max_results,
"search_depth": depth,
"topic": topic,
"include_images": include_images,
"include_answer": True,
"include_raw_content": include_raw_content,
}
if include_domains:
body["include_domains"] = include_domains
if exclude_domains:
body["exclude_domains"] = exclude_domains
headers = {"Content-Type": "application/json"}
data = make_request(endpoint, headers, body)
results = []
for item in data.get("results", [])[:max_results]:
result = {
"title": item.get("title", ""),
"url": item.get("url", ""),
"snippet": item.get("content", ""),
"score": round(item.get("score", 0.0), 3),
}
if include_raw_content and item.get("raw_content"):
result["raw_content"] = item["raw_content"]
results.append(result)
return {
"provider": "tavily",
"query": query,
"results": results,
"images": data.get("images", []),
"answer": data.get("answer", ""),
}
# =============================================================================
# Exa (Neural/Semantic Search)
# =============================================================================
def search_exa(
query: str,
api_key: str,
max_results: int = 5,
search_type: str = "neural",
category: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
similar_url: Optional[str] = None,
include_domains: Optional[List[str]] = None,
exclude_domains: Optional[List[str]] = None,
) -> dict:
"""Search using Exa (Neural/Semantic Search)."""
if similar_url:
endpoint = "https://api.exa.ai/findSimilar"
body = {
"url": similar_url,
"numResults": max_results,
"contents": {
"text": {"maxCharacters": 1000},
"highlights": True,
},
}
else:
endpoint = "https://api.exa.ai/search"
body = {
"query": query,
"numResults": max_results,
"type": search_type,
"contents": {
"text": {"maxCharacters": 1000},
"highlights": True,
},
}
if category:
body["category"] = category
if start_date:
body["startPublishedDate"] = start_date
if end_date:
body["endPublishedDate"] = end_date
if include_domains:
body["includeDomains"] = include_domains
if exclude_domains:
body["excludeDomains"] = exclude_domains
headers = {
"x-api-key": api_key,
"Content-Type": "application/json",
}
data = make_request(endpoint, headers, body)
results = []
for item in data.get("results", [])[:max_results]:
highlights = item.get("highlights", [])
snippet = highlights[0] if highlights else (item.get("text", "") or "")[:500]
results.append({
"title": item.get("title", ""),
"url": item.get("url", ""),
"snippet": snippet,
"score": round(item.get("score", 0.0), 3),
"published_date": item.get("publishedDate"),
"author": item.get("author"),
})
answer = results[0]["snippet"] if results else ""
return {
"provider": "exa",
"query": query if not similar_url else f"Similar to: {similar_url}",
"results": results,
"images": [],
"answer": answer,
}
# =============================================================================
# CLI
# =============================================================================
def main():
config = load_config()
parser = argparse.ArgumentParser(
description="Web Search Plus — Intelligent multi-provider search with smart auto-routing",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Intelligent Auto-Routing:
The query is analyzed using multi-signal detection to find the optimal provider:
Shopping Intent → Serper (Google)
"how much", "price of", "buy", product+brand combos, deals, specs
Research Intent → Tavily
"how does", "explain", "what is", analysis, pros/cons, tutorials
Discovery Intent → Exa (Neural)
"similar to", "companies like", "alternatives", URLs, startups, papers
Examples:
python3 search.py -q "iPhone 16 Pro Max price" # → Serper (shopping)
python3 search.py -q "how does HTTPS encryption work" # → Tavily (research)
python3 search.py -q "startups similar to Notion" # → Exa (discovery)
python3 search.py --explain-routing -q "your query" # Debug routing
Full docs: See README.md and SKILL.md
""",
)
# Common arguments
parser.add_argument(
"--provider", "-p",
choices=["serper", "tavily", "exa", "auto"],
help="Search provider (auto=intelligent routing)"
)
parser.add_argument(
"--query", "-q",
help="Search query"
)
parser.add_argument(
"--max-results", "-n",
type=int,
default=config.get("defaults", {}).get("max_results", 5),
help="Maximum results (default: 5)"
)
parser.add_argument(
"--images",
action="store_true",
help="Include images (Serper/Tavily)"
)
# Auto-routing options
parser.add_argument(
"--auto", "-a",
action="store_true",
help="Use intelligent auto-routing (default when no provider specified)"
)
parser.add_argument(
"--explain-routing",
action="store_true",
help="Show detailed routing analysis (debug mode)"
)
# Serper-specific
serper_config = config.get("serper", {})
parser.add_argument("--country", default=serper_config.get("country", "us"))
parser.add_argument("--language", default=serper_config.get("language", "en"))
parser.add_argument(
"--type",
dest="search_type",
default=serper_config.get("type", "search"),
choices=["search", "news", "images", "videos", "places", "shopping"]
)
parser.add_argument(
"--time-range",
choices=["hour", "day", "week", "month", "year"]
)
# Tavily-specific
tavily_config = config.get("tavily", {})
parser.add_argument(
"--depth",
default=tavily_config.get("depth", "basic"),
choices=["basic", "advanced"]
)
parser.add_argument(
"--topic",
default=tavily_config.get("topic", "general"),
choices=["general", "news"]
)
parser.add_argument("--raw-content", action="store_true")
# Exa-specific
exa_config = config.get("exa", {})
parser.add_argument(
"--exa-type",
default=exa_config.get("type", "neural"),
choices=["neural", "keyword"]
)
parser.add_argument(
"--category",
choices=[
"company", "research paper", "news", "pdf", "github",
"tweet", "personal site", "linkedin profile"
]
)
parser.add_argument("--start-date")
parser.add_argument("--end-date")
parser.add_argument("--similar-url")
# Domain filters
parser.add_argument("--include-domains", nargs="+")
parser.add_argument("--exclude-domains", nargs="+")
# Output
parser.add_argument("--compact", action="store_true")
args = parser.parse_args()
if not args.query and not args.similar_url:
parser.error("--query is required (unless using --similar-url with Exa)")
# Handle --explain-routing
if args.explain_routing:
if not args.query:
parser.error("--query is required for --explain-routing")
explanation = explain_routing(args.query, config)
indent = None if args.compact else 2
print(json.dumps(explanation, indent=indent, ensure_ascii=False))
return
# Determine provider
if args.provider == "auto" or (args.provider is None and not args.similar_url):
if args.query:
routing = auto_route_provider(args.query, config)
provider = routing["provider"]
routing_info = {
"auto_routed": True,
"provider": provider,
"confidence": routing["confidence"],
"confidence_level": routing["confidence_level"],
"reason": routing["reason"],
"top_signals": routing["top_signals"],
"scores": routing["scores"],
}
else:
provider = "exa"
routing_info = {
"auto_routed": True,
"provider": "exa",
"confidence": 1.0,
"confidence_level": "high",
"reason": "similar_url_specified",
}
else:
provider = args.provider or "serper"
routing_info = {"auto_routed": False, "provider": provider}
# Build provider fallback list
auto_config = config.get("auto_routing", {})
provider_priority = auto_config.get("provider_priority", ["serper", "tavily", "exa"])
disabled_providers = auto_config.get("disabled_providers", [])
# Start with the selected provider, then try others in priority order
providers_to_try = [provider]
for p in provider_priority:
if p not in providers_to_try and p not in disabled_providers:
providers_to_try.append(p)
# Helper function to execute search for a provider
def execute_search(prov: str) -> Dict[str, Any]:
key = validate_api_key(prov, config)
if prov == "serper":
return search_serper(
query=args.query,
api_key=key,
max_results=args.max_results,
country=args.country,
language=args.language,
search_type=args.search_type,
time_range=args.time_range,
include_images=args.images,
)
elif prov == "tavily":
return search_tavily(
query=args.query,
api_key=key,
max_results=args.max_results,
depth=args.depth,
topic=args.topic,
include_domains=args.include_domains,
exclude_domains=args.exclude_domains,
include_images=args.images,
include_raw_content=args.raw_content,
)
elif prov == "exa":
return search_exa(
query=args.query or "",
api_key=key,
max_results=args.max_results,
search_type=args.exa_type,
category=args.category,
start_date=args.start_date,
end_date=args.end_date,
similar_url=args.similar_url,
include_domains=args.include_domains,
exclude_domains=args.exclude_domains,
)
else:
raise ValueError(f"Unknown provider: {prov}")
# Try providers with fallback on error
errors = []
result = None
successful_provider = None
for current_provider in providers_to_try:
try:
result = execute_search(current_provider)
successful_provider = current_provider
break # Success! Exit the loop
except Exception as e:
error_msg = str(e)
errors.append({"provider": current_provider, "error": error_msg})
# Log fallback attempt to stderr
if len(providers_to_try) > 1:
remaining = [p for p in providers_to_try if p != current_provider and p not in [err["provider"] for err in errors]]
if remaining:
print(json.dumps({
"fallback": True,
"failed_provider": current_provider,
"error": error_msg,
"trying_next": remaining[0] if remaining else None
}), file=sys.stderr)
continue # Try next provider
if result is not None:
# Update routing info if we fell back to a different provider
if successful_provider != provider:
routing_info["fallback_used"] = True
routing_info["original_provider"] = provider
routing_info["provider"] = successful_provider
routing_info["fallback_errors"] = errors[:-1] if errors else []
result["routing"] = routing_info
indent = None if args.compact else 2
print(json.dumps(result, indent=indent, ensure_ascii=False))
else:
# All providers failed
error_result = {
"error": "All providers failed",
"provider": provider,
"query": args.query,
"routing": routing_info,
"provider_errors": errors,
}
print(json.dumps(error_result, indent=2), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Web Search Plus - Interactive Setup Wizard
==========================================
Runs on first use (when no config.json exists) to configure providers and API keys.
Creates config.json with your settings. API keys are stored locally only.
Usage:
python3 scripts/setup.py # Interactive setup
python3 scripts/setup.py --reset # Reset and reconfigure
"""
import json
import os
import sys
from pathlib import Path
# ANSI colors for terminal output
class Colors:
HEADER = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BOLD = '\033[1m'
DIM = '\033[2m'
RESET = '\033[0m'
def color(text: str, c: str) -> str:
"""Wrap text in color codes."""
return f"{c}{text}{Colors.RESET}"
def print_header():
"""Print the setup wizard header."""
print()
print(color("╔════════════════════════════════════════════════════════════╗", Colors.CYAN))
print(color("║ 🔍 Web Search Plus - Setup Wizard ║", Colors.CYAN))
print(color("╚════════════════════════════════════════════════════════════╝", Colors.CYAN))
print()
print(color("This wizard will help you configure your search providers.", Colors.DIM))
print(color("API keys are stored locally in config.json (gitignored).", Colors.DIM))
print()
def print_provider_info():
"""Print information about each provider."""
print(color("📚 Available Providers:", Colors.BOLD))
print()
providers = [
{
"name": "Serper",
"emoji": "🔎",
"best_for": "Google results, shopping, local businesses, news",
"free_tier": "2,500 queries/month",
"signup": "https://serper.dev",
"strengths": ["Fastest response times", "Product prices & specs", "Knowledge Graph", "Local business data"]
},
{
"name": "Tavily",
"emoji": "📖",
"best_for": "Research, explanations, in-depth analysis",
"free_tier": "1,000 queries/month",
"signup": "https://tavily.com",
"strengths": ["AI-synthesized answers", "Full page content", "Domain filtering", "Academic research"]
},
{
"name": "Exa",
"emoji": "🧠",
"best_for": "Semantic search, finding similar content, discovery",
"free_tier": "1,000 queries/month",
"signup": "https://exa.ai",
"strengths": ["Neural/semantic understanding", "Similar page discovery", "Startup/company finder", "Date filtering"]
}
]
for p in providers:
print(f" {p['emoji']} {color(p['name'], Colors.BOLD)}")
print(f" Best for: {color(p['best_for'], Colors.GREEN)}")
print(f" Free tier: {p['free_tier']}")
print(f" Sign up: {color(p['signup'], Colors.BLUE)}")
print()
def ask_yes_no(prompt: str, default: bool = True) -> bool:
"""Ask a yes/no question."""
suffix = "[Y/n]" if default else "[y/N]"
while True:
response = input(f"{prompt} {color(suffix, Colors.DIM)}: ").strip().lower()
if response == "":
return default
if response in ("y", "yes"):
return True
if response in ("n", "no"):
return False
print(color(" Please enter 'y' or 'n'", Colors.YELLOW))
def ask_choice(prompt: str, options: list, default: str = None) -> str:
"""Ask user to choose from a list of options."""
print(f"\n{prompt}")
for i, opt in enumerate(options, 1):
marker = color("→", Colors.GREEN) if opt == default else " "
print(f" {marker} {i}. {opt}")
while True:
hint = f" [default: {default}]" if default else ""
response = input(f"Enter number (1-{len(options)}){color(hint, Colors.DIM)}: ").strip()
if response == "" and default:
return default
try:
idx = int(response)
if 1 <= idx <= len(options):
return options[idx - 1]
except ValueError:
pass
print(color(f" Please enter a number between 1 and {len(options)}", Colors.YELLOW))
def ask_api_key(provider: str, signup_url: str) -> str:
"""Ask for an API key with validation."""
print()
print(f" {color(f'Get your {provider} API key:', Colors.DIM)} {color(signup_url, Colors.BLUE)}")
while True:
key = input(f" Enter your {provider} API key: ").strip()
if not key:
print(color(" ⚠️ No key entered. This provider will be disabled.", Colors.YELLOW))
return None
# Basic validation
if len(key) < 10:
print(color(" ⚠️ Key seems too short. Please check and try again.", Colors.YELLOW))
continue
# Mask key for confirmation
masked = key[:4] + "..." + key[-4:] if len(key) > 12 else key[:2] + "..."
print(color(f" ✓ Key saved: {masked}", Colors.GREEN))
return key
def ask_result_count() -> int:
"""Ask for default result count."""
options = ["3 (fast, minimal)", "5 (balanced - recommended)", "10 (comprehensive)"]
choice = ask_choice("Default number of results per search?", options, "5 (balanced - recommended)")
if "3" in choice:
return 3
elif "10" in choice:
return 10
return 5
def run_setup(skill_dir: Path, force_reset: bool = False):
"""Run the interactive setup wizard."""
config_path = skill_dir / "config.json"
example_path = skill_dir / "config.example.json"
# Check if config already exists
if config_path.exists() and not force_reset:
print(color("✓ config.json already exists!", Colors.GREEN))
print()
if not ask_yes_no("Do you want to reconfigure?", default=False):
print(color("Setup cancelled. Your existing config is unchanged.", Colors.DIM))
return False
print()
print_header()
print_provider_info()
# Load example config as base
if example_path.exists():
with open(example_path) as f:
config = json.load(f)
else:
config = {
"defaults": {"provider": "serper", "max_results": 5},
"auto_routing": {"enabled": True, "fallback_provider": "serper"},
"serper": {},
"tavily": {},
"exa": {}
}
# Remove any existing API keys from example
for provider in ["serper", "tavily", "exa"]:
if provider in config:
config[provider].pop("api_key", None)
enabled_providers = []
# ===== Question 1: Which providers to enable =====
print(color("─" * 60, Colors.DIM))
print(color("\n📋 Step 1: Choose Your Providers\n", Colors.BOLD))
print("Select which search providers you want to enable.")
print(color("(You need at least one API key to use this skill)", Colors.DIM))
print()
providers_info = {
"serper": ("Serper", "https://serper.dev", "Google results, shopping, local"),
"tavily": ("Tavily", "https://tavily.com", "Research, explanations, analysis"),
"exa": ("Exa", "https://exa.ai", "Semantic search, similar content")
}
for provider, (name, url, desc) in providers_info.items():
print(f" {color(name, Colors.BOLD)}: {desc}")
if ask_yes_no(f" Enable {name}?", default=True):
# ===== Question 2: API key for each enabled provider =====
api_key = ask_api_key(name, url)
if api_key:
config[provider]["api_key"] = api_key
enabled_providers.append(provider)
else:
print(color(f" → {name} disabled (no API key)", Colors.DIM))
else:
print(color(f" → {name} disabled", Colors.DIM))
print()
if not enabled_providers:
print()
print(color("⚠️ No providers enabled!", Colors.RED))
print("You need at least one API key to use web-search-plus.")
print("Run this setup again when you have an API key.")
return False
# ===== Question 3: Default provider =====
print(color("─" * 60, Colors.DIM))
print(color("\n⚙️ Step 2: Default Settings\n", Colors.BOLD))
if len(enabled_providers) > 1:
default_provider = ask_choice(
"Which provider should be the default for general queries?",
enabled_providers,
enabled_providers[0]
)
else:
default_provider = enabled_providers[0]
print(f"Default provider: {color(default_provider, Colors.GREEN)} (only one enabled)")
config["defaults"]["provider"] = default_provider
config["auto_routing"]["fallback_provider"] = default_provider
# ===== Question 4: Auto-routing =====
print()
print(color("Auto-routing", Colors.BOLD) + " automatically picks the best provider for each query:")
print(color(" • 'iPhone price' → Serper (shopping intent)", Colors.DIM))
print(color(" • 'how does TCP work' → Tavily (research intent)", Colors.DIM))
print(color(" • 'companies like Stripe' → Exa (discovery intent)", Colors.DIM))
print()
auto_routing = ask_yes_no("Enable auto-routing?", default=True)
config["auto_routing"]["enabled"] = auto_routing
if not auto_routing:
print(color(f" → All queries will use {default_provider}", Colors.DIM))
# ===== Question 5: Result count =====
print()
max_results = ask_result_count()
config["defaults"]["max_results"] = max_results
# Set disabled providers
all_providers = ["serper", "tavily", "exa"]
disabled = [p for p in all_providers if p not in enabled_providers]
config["auto_routing"]["disabled_providers"] = disabled
# ===== Save config =====
print()
print(color("─" * 60, Colors.DIM))
print(color("\n💾 Saving Configuration\n", Colors.BOLD))
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
print(color(f"✓ Configuration saved to: {config_path}", Colors.GREEN))
print()
# ===== Summary =====
print(color("📋 Configuration Summary:", Colors.BOLD))
print(f" Enabled providers: {', '.join(enabled_providers)}")
print(f" Default provider: {default_provider}")
print(f" Auto-routing: {'enabled' if auto_routing else 'disabled'}")
print(f" Results per search: {max_results}")
print()
# ===== Test suggestion =====
print(color("🚀 Ready to search! Try:", Colors.BOLD))
print(color(f" python3 scripts/search.py -q \"your query here\"", Colors.CYAN))
print()
return True
def check_first_run(skill_dir: Path) -> bool:
"""Check if this is the first run (no config.json)."""
config_path = skill_dir / "config.json"
return not config_path.exists()
def main():
# Determine skill directory
script_path = Path(__file__).resolve()
skill_dir = script_path.parent.parent
# Check for --reset flag
force_reset = "--reset" in sys.argv
# Check for --check flag (just check if setup needed)
if "--check" in sys.argv:
if check_first_run(skill_dir):
print("Setup required: config.json not found")
sys.exit(1)
else:
print("Setup complete: config.json exists")
sys.exit(0)
# Run setup
success = run_setup(skill_dir, force_reset)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
#!/bin/bash
# Test Auto-Routing Feature
# Tests various query types to verify routing works correctly
# Load from environment or .env file
if [ -f .env ]; then
source .env
fi
# Check required keys
if [ -z "$SERPER_API_KEY" ]; then
echo "Error: SERPER_API_KEY not set. Copy .env.example to .env and add your keys."
exit 1
fi
echo "Testing auto-routing..."
python3 scripts/search.py -q "buy iPhone 15 price" --auto
python3 scripts/search.py -q "how does quantum computing work" --auto
python3 scripts/search.py -q "companies like Stripe" --auto