Skill: nitinzadpe/nz-perplexity-search (version 1.0.1)

--- name: perplexity-search version: 1.0.0 description: Search the web with Perplexity Sonar API for current information, citations, and web-grounded answers. homepage: https://clawhub.ai metadata: {"clawdbot":{"emoji":"🔎","requires":{"bins":["python3"],"env":["PERPLEXITY_API_KEY"]},"primaryEnv":"PERPLEXITY_API_KEY","files":["scripts/*"]}} --- # Perplexity Search Use this skill to search the web via the Perplexity Sonar API. It returns accurate, cited, real-time answers grounded in current web sources. ## When to use this skill Invoke this skill automatically when the user asks about: - Current events, recent news, or anything that may have changed recently - Product releases, software versions, or API documentation - Research questions that benefit from citations and web sources - Comparisons, pricing, or availability that requires live data - Any question where freshness or sourcing matters **Trigger phrases:** "search for", "look up", "find out", "what's the latest", "current", "recent", "news about", "web search", "perplexity search" ## Setup Set your Perplexity API key in the environment before starting OpenClaw: ```bash # Add to ~/.openclaw/.env PERPLEXITY_API_KEY=pplx-your-key-here ``` Get a key at https://www.perplexity.ai/settings/api ## Command ```bash python3 {baseDir}/scripts/perplexity_search.py --query "<the user's question>" ``` ### Optional flags | Flag | Default | Description | |------|---------|-------------| | `--model sonar` | sonar | Fast, general-purpose search | | `--model sonar-pro` | — | Higher quality synthesis (costs more) | | `--model sonar-reasoning` | — | Step-by-step reasoning | | `--domains example.com` | — | Comma-separated domain allowlist | | `--max-results 8` | 8 | Cap displayed search results | | `--temperature 0.2` | 0.2 | Lower = more factual | | `--json` | — | Print raw JSON response | ## Usage examples ```bash # General search python3 {baseDir}/scripts/perplexity_search.py --query "latest AI agent frameworks 2025" # Higher quality synthesis python3 {baseDir}/scripts/perplexity_search.py --query "compare Perplexity vs Tavily search APIs" --model sonar-pro # Restrict to a specific site python3 {baseDir}/scripts/perplexity_search.py --query "PostgreSQL 17 release notes" --domains postgresql.org # Reasoning mode for complex topics python3 {baseDir}/scripts/perplexity_search.py --query "why did SVB fail" --model sonar-reasoning ``` ## Output The script prints a structured markdown report with: 1. **Answer** — concise, web-grounded response 2. **Citations** — numbered source URLs 3. **Search Results** — titles, URLs, and snippets 4. **Related Questions** — follow-up ideas 5. **Usage** — token and cost metadata ## Agent workflow 1. Identify the user's search intent from their message. 2. Extract a clean, specific query (avoid filler words). 3. Choose `sonar` for speed, `sonar-pro` for depth. 4. Run the command with `python3 {baseDir}/scripts/perplexity_search.py --query "..."`. 5. Present the Answer and Citations to the user in a readable format. 6. Offer related questions as follow-up options. ## Security notes - The script reads `PERPLEXITY_API_KEY` from the environment only — never from arguments. - The API key is never logged or printed. - All external calls go only to `https://api.perplexity.ai/chat/completions`. - No local files are written. ## SECURITY MANIFEST - Environment variables accessed: PERPLEXITY_API_KEY (only) - External endpoints called: https://api.perplexity.ai/chat/completions (only) - Local files read: none - Local files written: none

path: README.md
content: # 🔎 Perplexity Search — OpenClaw Skill

Search the web with the **Perplexity Sonar API** from inside OpenClaw. Get grounded, cited, real-time answers without leaving your agent session.

## What this skill does

- Sends your query to Perplexity's `sonar` or `sonar-pro` models
- Returns a structured markdown report: **Answer → Citations → Search Results → Related Questions**
- Works with zero external Python packages (pure standard library)
- Auto-invoked by the agent on web-search intent — no slash command needed

## Requirements

| Requirement | Value |
|-------------|-------|
| Binary | `python3` (3.8+) |
| Environment variable | `PERPLEXITY_API_KEY` |
| External endpoint | `https://api.perplexity.ai/chat/completions` |

## Install

```bash
clawhub install perplexity-search
```

Then add your API key to `~/.openclaw/.env`:

```bash
PERPLEXITY_API_KEY=pplx-your-key-here
```

Get a key at → https://www.perplexity.ai/settings/api

Restart OpenClaw (or start a new session) to activate the skill.

## Usage

The agent invokes this skill automatically when you ask about anything web-related:

> "What's the latest on GPT-5?"  
> "Search for the best LMS platforms for distance education"  
> "Find the current Perplexity API pricing"

Or reference it explicitly:

> "Use Perplexity to find recent news about AI agents"

## Manual test (outside OpenClaw)

```bash
python3 skills/perplexity-search/scripts/perplexity_search.py \
  --query "OpenClaw latest features"
```

## Skill folder layout

```
perplexity-search/
├── SKILL.md               ← Agent instructions + frontmatter
└── scripts/
    └── perplexity_search.py  ← Python search script (no dependencies)
```

## Security

- Only `PERPLEXITY_API_KEY` is accessed from the environment.
- The key is never printed, logged, or passed as a command-line argument.
- The only external call is to `https://api.perplexity.ai/chat/completions`.
- No local files are read or written.

---

Licensed MIT-0 · Published to ClawHub


path: scripts/perplexity_search.py
content: #!/usr/bin/env python3
"""Perplexity Sonar web search for OpenClaw / ClawHub.

Reads PERPLEXITY_API_KEY from the environment.
Uses only Python standard library — no pip installs required.

Usage:
    python3 perplexity_search.py --query "your question here"
    python3 perplexity_search.py --query "..." --model sonar-pro
    python3 perplexity_search.py --query "..." --domains docs.example.com
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import textwrap
import urllib.error
import urllib.request
from typing import Any

# ── Constants ──────────────────────────────────────────────────────────────────

API_URL = "https://api.perplexity.ai/chat/completions"
DEFAULT_MODEL = "sonar"

ALLOWED_MODELS = {
    "sonar",
    "sonar-pro",
    "sonar-deep-research",
    "sonar-reasoning",
    "sonar-reasoning-pro",
}

SYSTEM_PROMPT = (
    "You are a web research assistant. "
    "Give a precise, concise answer grounded in recent web sources. "
    "Include enough detail to be directly useful, but avoid filler."
)


# ── CLI ────────────────────────────────────────────────────────────────────────

def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        description="Search the web with Perplexity Sonar.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    p.add_argument("--query", required=True, help="Search query or user question")
    p.add_argument(
        "--model",
        default=DEFAULT_MODEL,
        choices=sorted(ALLOWED_MODELS),
        metavar="MODEL",
        help=(
            f"Perplexity model. One of: {', '.join(sorted(ALLOWED_MODELS))}. "
            f"Default: {DEFAULT_MODEL}"
        ),
    )
    p.add_argument(
        "--temperature",
        type=float,
        default=0.2,
        help="Sampling temperature 0–1. Lower = more factual. Default: 0.2",
    )
    p.add_argument(
        "--domains",
        default="",
        help="Comma-separated domain allowlist, e.g. docs.python.org,github.com",
    )
    p.add_argument(
        "--max-results",
        type=int,
        default=8,
        help="Max search results to display. Default: 8",
    )
    p.add_argument(
        "--json",
        dest="raw_json",
        action="store_true",
        help="Print raw JSON response instead of formatted markdown",
    )
    return p.parse_args()


# ── Auth ───────────────────────────────────────────────────────────────────────

def require_api_key() -> str:
    key = os.environ.get("PERPLEXITY_API_KEY", "").strip()
    if not key:
        print(
            "ERROR: PERPLEXITY_API_KEY environment variable is not set.\n"
            "Add it to ~/.openclaw/.env:\n"
            "  PERPLEXITY_API_KEY=pplx-your-key-here\n"
            "Get a key at: https://www.perplexity.ai/settings/api",
            file=sys.stderr,
        )
        sys.exit(2)
    return key


# ── Request ────────────────────────────────────────────────────────────────────

def build_payload(args: argparse.Namespace) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "model": args.model,
        "temperature": args.temperature,
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": args.query.strip()},
        ],
    }
    domains = [d.strip() for d in args.domains.split(",") if d.strip()]
    if domains:
        payload["search_domain_filter"] = domains
    return payload


def call_api(api_key: str, payload: dict[str, Any]) -> dict[str, Any]:
    req = urllib.request.Request(
        API_URL,
        data=json.dumps(payload).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        print(f"HTTP {exc.code} from Perplexity API:\n{body}", file=sys.stderr)
        sys.exit(3)
    except urllib.error.URLError as exc:
        print(f"Network error reaching Perplexity API: {exc}", file=sys.stderr)
        sys.exit(4)


# ── Formatting ─────────────────────────────────────────────────────────────────

def _text(response: dict[str, Any]) -> str:
    choices = response.get("choices") or []
    if not choices:
        return ""
    return (((choices[0] or {}).get("message") or {}).get("content") or "").strip()


def format_output(response: dict[str, Any], max_results: int, model: str) -> str:
    answer = _text(response)
    citations: list[str] = response.get("citations") or []
    search_results: list[dict] = response.get("search_results") or []
    related: list[str] = response.get("related_questions") or []
    usage: dict = response.get("usage") or {}

    lines: list[str] = []

    # ── Header
    lines += [f"# 🔎 Perplexity Search  ·  model: {model}", ""]

    # ── Answer
    if answer:
        lines += ["## Answer", "", answer, ""]

    # ── Citations
    if citations:
        lines += ["## Citations", ""]
        for i, url in enumerate(citations, 1):
            lines.append(f"{i}. {url}")
        lines.append("")

    # ── Search results
    if search_results:
        lines += ["## Search Results", ""]
        for i, item in enumerate(search_results[:max_results], 1):
            title = item.get("title") or "Untitled"
            url = item.get("url") or item.get("origin_url") or ""
            date = item.get("date") or item.get("last_updated") or ""
            snippet = (item.get("snippet") or "").strip()
            if snippet:
                snippet = textwrap.shorten(snippet, width=260, placeholder="…")

            header = f"**{i}. {title}**"
            if date:
                header += f" _({date})_"
            lines.append(header)
            if url:
                lines.append(f"   <{url}>")
            if snippet:
                lines.append(f"   {snippet}")
            lines.append("")

    # ── Related questions
    if related:
        lines += ["## Related Questions", ""]
        for q in related[:6]:
            lines.append(f"- {q}")
        lines.append("")

    # ── Usage summary
    if usage:
        parts = []
        if (t := usage.get("total_tokens")) is not None:
            parts.append(f"tokens: {t}")
        if (n := usage.get("num_search_queries")) is not None:
            parts.append(f"queries: {n}")
        if (c := (usage.get("cost") or {}).get("total_cost")) is not None:
            parts.append(f"cost: ${c:.4f}")
        if parts:
            lines += [f"_Usage — {' · '.join(parts)}_", ""]

    if not answer and not search_results:
        lines.append("_No results returned from Perplexity._")

    return "\n".join(lines).rstrip() + "\n"


# ── Main ───────────────────────────────────────────────────────────────────────

def main() -> None:
    args = parse_args()
    [REDACTED_SECRET])
    payload = build_payload(args)
    response = call_api(api_key, payload)

    if args.raw_json:
        print(json.dumps(response, indent=2, ensure_ascii=False))
        return

    print(format_output(response, max_results=args.max_results, model=args.model))


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)