web_search
Search the web and ingest results as wiki pages
Install
npx skills add https://github.com/axoviq-ai/synthadoc/tree/main/synthadoc/skills/web_search
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install axoviq-ai-synthadoc@llmmart
git clone https://github.com/axoviq-ai/synthadoc.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole axoviq-ai/synthadoc collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Web Search Skill
Accepts a natural language query, calls the Tavily AI search API, and returns the top matching URLs. Your agent receives those URLs and decides what to do with them — fetch each one, display them, pass them to another skill, etc.
Setup
1. Install the dependency:
pip install tavily-python
2. Set your Tavily API key (free tier: 1,000 searches/month — sign up at https://tavily.com, no credit card required):
# macOS / Linux
export TAVILY_API_KEY="tvly-your-key-here"
# Windows (Command Prompt)
set TAVILY_API_KEY=tvly-your-key-here
# Windows (PowerShell)
$env:TAVILY_API_KEY = "tvly-your-key-here"
3. Optional — cap the number of results (default: 20):
export SYNTHADOC_WEB_SEARCH_MAX_RESULTS=10
Standalone usage
import asyncio
from synthadoc.skills.web_search.scripts.main import WebSearchSkill
skill = WebSearchSkill()
async def main():
result = await skill.extract("search for: transformer architecture papers")
urls = result.metadata["child_sources"] # list[str] — top matching URLs
query = result.metadata["query"] # "transformer architecture papers"
print(f"Found {len(urls)} URLs for '{query}':")
for url in urls:
print(" ", url)
asyncio.run(main())
result.text is always empty — the skill is a discovery step that returns
URLs, not page content. Pass the URLs to the url or youtube skill (or
your own HTTP client) to fetch content.
Intent prefixes
The skill strips a leading intent phrase before sending the query to Tavily:
| Input | Query sent to Tavily |
|---|---|
search for: RAG evaluation |
RAG evaluation |
find on the web: LLM benchmarks |
LLM benchmarks |
look up quantum computing |
quantum computing |
youtube: Karpathy transformers |
Karpathy transformers (YouTube only) |
搜索: 深度学习架构 |
深度学习架构 |
YouTube-specific prefixes (youtube:, search youtube:, youtube video:,
etc.) restrict the Tavily search to youtube.com and youtu.be.
CJK intent phrases supported: 查找, 搜索, 网络搜索, 在网上查, 查一下
Domain filtering
A built-in blocklist skips sites that block automated HTTP clients:
reddit.com, medium.com, quora.com, twitter.com/x.com,
linkedin.com, wikipedia.org, IEEE Xplore, ACM DL, and common
subscription-only academic publishers.
If SYNTHADOC_WIKI_ROOT is set, the skill also loads
$SYNTHADOC_WIKI_ROOT/.synthadoc/blocked_domains.json (a JSON array of
domain strings) to extend the blocklist at runtime.
Scripts
scripts/main.py—WebSearchSkill: intent parsing, domain filtering, returnschild_sourcesin metadatascripts/fetcher.py— thin async wrapper aroundAsyncTavilyClient
Assets
assets/search-providers.json— search provider registry (currently Tavily)
Using with full Synthadoc
When running inside Synthadoc, the Orchestrator reads child_sources from
the result metadata and automatically enqueues each URL as a separate ingest
job, which are then processed by the url or youtube skill. No additional
setup is required beyond the env vars above.
Files (synthadoc)
-
assets
-
search-providers.json 256 B
{ "_comment": "Search provider configuration", "providers": [ { "name": "tavily", "api_key_env": "TAVILY_API_KEY", "free_tier": true, "free_limit": "1000 searches/month", "signup_url": "https://tavily.com" } ] }
-
-
scripts
-
fetcher.py 645 B
# SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 Paul Chen / axoviq.com """Tavily search client wrapper for web_search skill.""" from __future__ import annotations async def search_tavily( query: str, max_results: int, api_key: str, include_domains: list[str] | None = None, ) -> dict: """Call Tavily search API and return raw response dict.""" from tavily import AsyncTavilyClient client = AsyncTavilyClient(api_key=api_key) kwargs: dict = {"max_results": max_results} if include_domains: kwargs["include_domains"] = include_domains return await client.search(query, **kwargs) -
main.py 4.7 KB
# SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 Paul Chen / axoviq.com from __future__ import annotations import json import os import re from pathlib import Path from urllib.parse import urlparse from synthadoc.skills.base import BaseSkill, ExtractedContent, SkillMeta, Triggers # Matches all generic intents declared in SKILL.md; colon and leading whitespace optional _INTENT_RE = re.compile( r"^(search\s+for|find\s+on\s+the\s+web|look\s+up|web\s+search|browse):?\s*", re.IGNORECASE, ) # Matches YouTube-specific intent prefixes. All of these should search YouTube only. # "youtube Moore's Law" # "youtube video on transistors" # "youtube kids: Sesame Street" # "search for youtube: history of computing" # "search youtube: Moore's Law" # "youtube search: lectures on transformers" _YOUTUBE_INTENT_RE = re.compile( r"""^(?: search\s+(?:for\s+)?youtube(?:\s+for)? # search for youtube / search youtube / search youtube for | youtube\s+search # youtube search | youtube(?:\s+(?:video|kids|lecture|talk|channel|for))? # youtube / youtube video / youtube kids … )\s*:?\s*""", re.IGNORECASE | re.VERBOSE, ) # Domains passed to Tavily when a YouTube-specific search is detected _YOUTUBE_DOMAINS = ["youtube.com", "youtu.be"] _DEFAULT_MAX_RESULTS = 20 # Domains that block automated HTTP clients (Cloudflare, login walls, etc.). # URLs from these domains are skipped to prevent dead ingest jobs. _BLOCKED_DOMAINS = { # Require JavaScript/login — can't be fetched by a plain HTTP client "quora.com", "medium.com", "reddit.com", "facebook.com", "instagram.com", "twitter.com", "x.com", "linkedin.com", "tiktok.com", # Wikipedia blocks plain HTTP clients even with a browser User-Agent "wikipedia.org", # GitHub returns 429 for automated access on raw file/blob pages "github.com", # Require institutional/subscription access "ieeexplore.ieee.org", "dl.acm.org", "sciencedirect.com", "springer.com", "jstor.org", } def _load_dynamic_blocked() -> set[str]: """Load domains auto-blocked at runtime from .synthadoc/blocked_domains.json.""" wiki_root = os.environ.get("SYNTHADOC_WIKI_ROOT", "") if not wiki_root: return set() blocked_path = Path(wiki_root) / ".synthadoc" / "blocked_domains.json" if not blocked_path.exists(): return set() try: return set(json.loads(blocked_path.read_text(encoding="utf-8"))) except Exception: return set() class WebSearchSkill(BaseSkill): meta = SkillMeta( name="web_search", description="Search the web and ingest results as wiki pages", triggers=Triggers( extensions=[], intents=[ "search for", "find on the web", "look up", "web search", "browse", "youtube", "查找", "搜索", "网络搜索", "在网上查", "查一下", ], ), requires=["tavily-python"], ) async def extract(self, source: str) -> ExtractedContent: api_key = os.environ.get("TAVILY_API_KEY", "").strip() if not api_key: raise EnvironmentError( "[ERR-SKILL-004] TAVILY_API_KEY is not set. Get a free key at https://tavily.com " "and set it with: export TAVILY_API_KEY=<your-key>" ) max_results = int( os.environ.get("SYNTHADOC_WEB_SEARCH_MAX_RESULTS", _DEFAULT_MAX_RESULTS) ) youtube_match = _YOUTUBE_INTENT_RE.match(source) if youtube_match: query = source[youtube_match.end():].strip() or source include_domains: list[str] | None = _YOUTUBE_DOMAINS else: query = _INTENT_RE.sub("", source).strip() or source include_domains = None from synthadoc.skills.web_search.scripts.fetcher import search_tavily response = await search_tavily( query, max_results=max_results, api_key=api_key, include_domains=include_domains, ) all_blocked = _BLOCKED_DOMAINS | _load_dynamic_blocked() def _allowed(url: str) -> bool: host = urlparse(url).hostname or "" return not any(host == d or host.endswith("." + d) for d in all_blocked) child_sources = [ r["url"] for r in response.get("results", []) if r.get("url") and _allowed(r["url"]) ] return ExtractedContent( text="", source_path=source, metadata={ "child_sources": child_sources, "query": query, "results_count": len(child_sources), }, ) -
__init__.py 0 B
-
-
requirements.txt 14 B
tavily-python -
SKILL.md 3.6 KB
--- name: web_search version: "1.0" description: Search the web and ingest results as wiki pages entry: script: scripts/main.py class: WebSearchSkill triggers: extensions: [] intents: - "search for" - "find on the web" - "look up" - "web search" - "browse" - "youtube" - "查找" - "搜索" - "网络搜索" - "在网上查" - "查一下" requires: - tavily-python author: axoviq.com license: AGPL-3.0-or-later --- # Web Search Skill Accepts a natural language query, calls the Tavily AI search API, and returns the top matching URLs. Your agent receives those URLs and decides what to do with them — fetch each one, display them, pass them to another skill, etc. ## Setup **1. Install the dependency:** ```bash pip install tavily-python ``` **2. Set your Tavily API key** (free tier: 1,000 searches/month — sign up at https://tavily.com, no credit card required): ```bash # macOS / Linux export TAVILY_API_KEY="tvly-your-key-here" # Windows (Command Prompt) set TAVILY_API_KEY=tvly-your-key-here # Windows (PowerShell) $env:TAVILY_API_KEY = "tvly-your-key-here" ``` **3. Optional — cap the number of results** (default: 20): ```bash export SYNTHADOC_WEB_SEARCH_MAX_RESULTS=10 ``` ## Standalone usage ```python import asyncio from synthadoc.skills.web_search.scripts.main import WebSearchSkill skill = WebSearchSkill() async def main(): result = await skill.extract("search for: transformer architecture papers") urls = result.metadata["child_sources"] # list[str] — top matching URLs query = result.metadata["query"] # "transformer architecture papers" print(f"Found {len(urls)} URLs for '{query}':") for url in urls: print(" ", url) asyncio.run(main()) ``` `result.text` is always empty — the skill is a discovery step that returns URLs, not page content. Pass the URLs to the `url` or `youtube` skill (or your own HTTP client) to fetch content. ## Intent prefixes The skill strips a leading intent phrase before sending the query to Tavily: | Input | Query sent to Tavily | |---|---| | `search for: RAG evaluation` | `RAG evaluation` | | `find on the web: LLM benchmarks` | `LLM benchmarks` | | `look up quantum computing` | `quantum computing` | | `youtube: Karpathy transformers` | `Karpathy transformers` (YouTube only) | | `搜索: 深度学习架构` | `深度学习架构` | YouTube-specific prefixes (`youtube:`, `search youtube:`, `youtube video:`, etc.) restrict the Tavily search to `youtube.com` and `youtu.be`. CJK intent phrases supported: 查找, 搜索, 网络搜索, 在网上查, 查一下 ## Domain filtering A built-in blocklist skips sites that block automated HTTP clients: `reddit.com`, `medium.com`, `quora.com`, `twitter.com`/`x.com`, `linkedin.com`, `wikipedia.org`, IEEE Xplore, ACM DL, and common subscription-only academic publishers. If `SYNTHADOC_WIKI_ROOT` is set, the skill also loads `$SYNTHADOC_WIKI_ROOT/.synthadoc/blocked_domains.json` (a JSON array of domain strings) to extend the blocklist at runtime. ## Scripts - `scripts/main.py` — `WebSearchSkill`: intent parsing, domain filtering, returns `child_sources` in metadata - `scripts/fetcher.py` — thin async wrapper around `AsyncTavilyClient` ## Assets - `assets/search-providers.json` — search provider registry (currently Tavily) ## Using with full Synthadoc When running inside Synthadoc, the Orchestrator reads `child_sources` from the result metadata and automatically enqueues each URL as a separate ingest job, which are then processed by the `url` or `youtube` skill. No additional setup is required beyond the env vars above.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.