Claude Skill

web_search

Search the web and ingest results as wiki pages

LLM Mart · 0 points · 26 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download axoviq-ai-synthadoc-synthadoc_skills_web_search-0c32d4f.zip · 5 KB
Part of axoviq-ai/synthadoc — 10 skills

Install

skills CLI npx skills add https://github.com/axoviq-ai/synthadoc/tree/main/synthadoc/skills/web_search
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install axoviq-ai-synthadoc@llmmart
Git 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, 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.

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.

No comments yet.

Reviews (0)

No reviews yet.

Related