From d6f7b5f7dd3a49003bfeb944bdde5490ed212cd0 Mon Sep 17 00:00:00 2001 From: Tavily PR Agent Date: Wed, 22 Apr 2026 16:33:02 +0000 Subject: [PATCH 1/2] feat: add Tavily as parallel/fallback search provider --- SKILL.md | 20 ++++++-- requirements.txt | 2 + scripts/search.py | 118 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 requirements.txt diff --git a/SKILL.md b/SKILL.md index bff0245..9e130c5 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,13 +1,15 @@ --- name: google-pse-search description: > - Web search skill powered by Google PSE (Programmable Search Engine) API. + Web search skill powered by Google PSE (Programmable Search Engine) API + with optional Tavily search as a parallel/fallback provider. Primary search tool for all web queries — Korean and English alike. Better search quality than DuckDuckGo, especially for Korean content. Triggers: "search for", "find", "look up", news/current events, recent info, date-filtered searches, site-specific searches, any general web search request. Requires environment variables: GOOGLE_PSE_KEY, GOOGLE_CX_ID. - Falls back to duckduckgo-search only on quota exceeded (HTTP 429/403) or API errors. + Optional: TAVILY_API_KEY (enables --provider tavily and --provider auto fallback). + Falls back to Tavily (if configured) or duckduckgo-search on quota exceeded (HTTP 429/403). --- # Google PSE Search @@ -16,8 +18,11 @@ description: > ``` 1st: google-pse-search ← this skill (default for all web searches) + --provider google (default) uses Google PSE API + --provider tavily uses Tavily API directly + --provider auto uses Google PSE, falls back to Tavily on 403/429 2nd: web_fetch ← when URL is known (official docs, specific pages) -3rd: duckduckgo-search ← fallback on quota exceeded or API errors only +3rd: duckduckgo-search ← fallback when neither Google PSE nor Tavily is available ``` ## Basic Usage @@ -46,6 +51,12 @@ python $SKILL_DIR/scripts/search.py "query" --exact "must include" --exclude "ex # Pagination (page 2) python $SKILL_DIR/scripts/search.py "query" --start 11 + +# Use Tavily as search provider (requires TAVILY_API_KEY) +python $SKILL_DIR/scripts/search.py "query" --provider tavily + +# Auto mode: Google PSE with Tavily fallback on quota errors +python $SKILL_DIR/scripts/search.py "query" --provider auto ``` ## Options @@ -61,13 +72,14 @@ python $SKILL_DIR/scripts/search.py "query" --start 11 | `--site SITE` | — | Restrict to a specific site | | `--start N` | 1 | Start index for pagination | | `--raw` | — | Print raw JSON (debug) | +| `--provider` | google | Search provider: `google`, `tavily`, or `auto` | ## Error Handling | Error | Cause | Action | |-------|-------|--------| | Missing env vars | .env not configured | Set GOOGLE_PSE_KEY and GOOGLE_CX_ID | -| HTTP 403/429 | Quota exceeded | Use duckduckgo-search as fallback | +| HTTP 403/429 | Quota exceeded | Use `--provider auto` for Tavily fallback, or duckduckgo-search | | HTTP 400 | Invalid parameters | Check option values | | 0 results | Query or filter issue | Adjust query or remove filters | diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..851c936 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +tavily-python diff --git a/scripts/search.py b/scripts/search.py index f4d28c3..53cba15 100644 --- a/scripts/search.py +++ b/scripts/search.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Google PSE (Programmable Search Engine) search script. +Google PSE (Programmable Search Engine) search script with optional Tavily provider. Usage: python search.py "query" @@ -12,10 +12,15 @@ python search.py "query" --site example.com python search.py "query" --start 11 python search.py "query" --raw + python search.py "query" --provider tavily + python search.py "query" --provider auto Environment variables required: - GOOGLE_PSE_KEY API key - GOOGLE_CX_ID Programmable Search Engine ID + GOOGLE_PSE_KEY API key (for google provider) + GOOGLE_CX_ID Programmable Search Engine ID (for google provider) + +Optional environment variables: + TAVILY_API_KEY Tavily API key (enables tavily/auto provider) """ import argparse @@ -32,6 +37,82 @@ API_URL = "https://customsearch.googleapis.com/customsearch/v1" + +def get_tavily_client(): + """Return a TavilyClient if tavily-python is installed and TAVILY_API_KEY is set.""" + api_key = os.environ.get("TAVILY_API_KEY") + if not api_key: + return None + try: + from tavily import TavilyClient + return TavilyClient(api_key=api_key) + except ImportError: + return None + + +def tavily_search(args): + """Perform a search using the Tavily API and print results in the same format.""" + client = get_tavily_client() + if client is None: + print("Error: Tavily is not available.") + print(" Ensure TAVILY_API_KEY is set and tavily-python is installed:") + print(" pip install tavily-python") + sys.exit(1) + + kwargs = { + "query": args.query, + "max_results": min(max(1, args.num), 10), + "search_depth": "advanced", + } + + if args.site: + kwargs["include_domains"] = [args.site] + + try: + response = client.search(**kwargs) + except Exception as e: + print(f"Error: Tavily search failed: {e}") + sys.exit(1) + + if args.raw: + print(json.dumps(response, ensure_ascii=False, indent=2)) + return + + print(format_tavily_results(response, args)) + + +def format_tavily_results(data, args): + """Format Tavily results into the same Markdown format as Google PSE.""" + results = data.get("results", []) + + lang_code = args.lang.lower() + gl = args.gl if args.gl else LANG_MAP.get(lang_code, ("", lang_code))[1] + + conditions = [f"lang:{lang_code}", f"region:{gl}", "provider:tavily"] + if args.site: + conditions.append(f"site:{args.site}") + + lines = [] + lines.append(f'## Search Results: "{args.query}" ({len(results)} results)') + lines.append(f'> Filters: {" · ".join(conditions)}') + lines.append("") + + if not results: + lines.append("No results found. Try adjusting your query or removing filters.") + return "\n".join(lines) + + for i, item in enumerate(results, 1): + title = item.get("title", "(no title)") + url = item.get("url", "") + snippet = item.get("content", "").replace("\n", " ").strip() + lines.append(f"### {i}. [{title}]({url})") + if snippet: + lines.append(snippet) + lines.append("") + + return "\n".join(lines) + + LANG_MAP = { "ko": ("lang_ko", "kr"), "en": ("lang_en", "us"), @@ -131,6 +212,18 @@ def format_results(data, args): def search(args): + provider = getattr(args, "provider", "google") + + if provider == "tavily": + tavily_search(args) + return + + if provider == "auto" and not os.environ.get("GOOGLE_PSE_KEY"): + # No Google credentials; try Tavily directly + if get_tavily_client() is not None: + tavily_search(args) + return + api_key, cx_id = get_env() params = build_params(args, api_key, cx_id) @@ -143,6 +236,15 @@ def search(args): print("Error: Network connection failed") sys.exit(1) + # On quota/rate-limit errors, fall back to Tavily when provider=auto + if resp.status_code in (403, 429) and provider == "auto": + client = get_tavily_client() + if client is not None: + print(f"# Google PSE returned {resp.status_code}, falling back to Tavily...\n", + file=sys.stderr) + tavily_search(args) + return + if resp.status_code == 400: err = resp.json().get("error", {}).get("message", "") print(f"Error: Bad request (400): {err}") @@ -150,13 +252,13 @@ def search(args): elif resp.status_code == 403: err = resp.json().get("error", {}).get("message", "") if "quota" in err.lower() or "limit" in err.lower(): - print("Error: Daily quota exceeded (100 requests/day). Use duckduckgo-search as fallback.") + print("Error: Daily quota exceeded (100 requests/day). Use --provider tavily or --provider auto as fallback.") else: print(f"Error: Access denied (403). Check your API key or CX ID.") print(f" Details: {err}") sys.exit(1) elif resp.status_code == 429: - print("Error: Rate limit exceeded (429). Use duckduckgo-search as fallback.") + print("Error: Rate limit exceeded (429). Use --provider tavily or --provider auto as fallback.") sys.exit(1) elif not resp.ok: print(f"Error: API error ({resp.status_code}): {resp.text[:200]}") @@ -185,6 +287,12 @@ def main(): parser.add_argument("--site", default="", help="Restrict search to a specific site") parser.add_argument("--start", type=int, default=1, help="Start index for pagination (default: 1)") parser.add_argument("--raw", action="store_true", help="Print raw JSON response (debug)") + parser.add_argument( + "--provider", + choices=["google", "tavily", "auto"], + default="google", + help="Search provider: google (default), tavily, or auto (Google with Tavily fallback)", + ) args = parser.parse_args() search(args) From a67edfa30a722c70214734abe17adb39173a4d06 Mon Sep 17 00:00:00 2001 From: Tavily PR Agent Date: Wed, 22 Apr 2026 16:35:33 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20review=20feedback=20(attem?= =?UTF-8?q?pt=202)=20=E2=80=94=20Google=20PSE=20Search=20Skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ SKILL.md | 3 ++- requirements.txt | 4 ++-- scripts/search.py | 41 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/SKILL.md b/SKILL.md index 9e130c5..adbbdac 100644 --- a/SKILL.md +++ b/SKILL.md @@ -66,7 +66,7 @@ python $SKILL_DIR/scripts/search.py "query" --provider auto | `--num N` | 5 | Number of results (1–10) | | `--lang LANG` | ko | Language code (ko/en/ja/zh) | | `--gl GL` | auto by lang | Region code override | -| `--date DATE` | — | Date restriction (d7/m1/y1 etc.) | +| `--date DATE` | — | Date restriction (d7/m1/y1 etc.). Tavily maps d1→day, d7/w1→week, m1→month, y1→year; other values are unsupported. | | `--exact PHRASE` | — | Phrase that must appear in results | | `--exclude TERM` | — | Term to exclude from results | | `--site SITE` | — | Restrict to a specific site | @@ -82,6 +82,7 @@ python $SKILL_DIR/scripts/search.py "query" --provider auto | HTTP 403/429 | Quota exceeded | Use `--provider auto` for Tavily fallback, or duckduckgo-search | | HTTP 400 | Invalid parameters | Check option values | | 0 results | Query or filter issue | Adjust query or remove filters | +| Unsupported flag warning | --exact/--exclude/--start used with Tavily | These flags are Google-only; a stderr warning is emitted | ## API Reference diff --git a/requirements.txt b/requirements.txt index 851c936..f298c62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -requests -tavily-python +requests>=2.28 +tavily-python>=0.5 diff --git a/scripts/search.py b/scripts/search.py index 53cba15..4a40979 100644 --- a/scripts/search.py +++ b/scripts/search.py @@ -50,15 +50,45 @@ def get_tavily_client(): return None -def tavily_search(args): +DATE_TO_TIME_RANGE = { + "d1": "day", + "d7": "week", + "w1": "week", + "m1": "month", + "y1": "year", +} + + +def _warn_unsupported_tavily_flags(args): + """Emit a stderr warning when Google-only flags are used with Tavily.""" + unsupported = [] + if args.date and args.date not in DATE_TO_TIME_RANGE: + unsupported.append(f"--date {args.date}") + if args.exact: + unsupported.append("--exact") + if args.exclude: + unsupported.append("--exclude") + if args.start > 1: + unsupported.append("--start") + if unsupported: + print( + f"Warning: {', '.join(unsupported)} not fully supported by Tavily and will be ignored.", + file=sys.stderr, + ) + + +def tavily_search(args, client=None): """Perform a search using the Tavily API and print results in the same format.""" - client = get_tavily_client() + if client is None: + client = get_tavily_client() if client is None: print("Error: Tavily is not available.") print(" Ensure TAVILY_API_KEY is set and tavily-python is installed:") print(" pip install tavily-python") sys.exit(1) + _warn_unsupported_tavily_flags(args) + kwargs = { "query": args.query, "max_results": min(max(1, args.num), 10), @@ -68,6 +98,11 @@ def tavily_search(args): if args.site: kwargs["include_domains"] = [args.site] + if args.date: + time_range = DATE_TO_TIME_RANGE.get(args.date) + if time_range: + kwargs["time_range"] = time_range + try: response = client.search(**kwargs) except Exception as e: @@ -242,7 +277,7 @@ def search(args): if client is not None: print(f"# Google PSE returned {resp.status_code}, falling back to Tavily...\n", file=sys.stderr) - tavily_search(args) + tavily_search(args, client=client) return if resp.status_code == 400: