Skill: bibaofeng/aisa-multi-search-engine (version 1.0.2)

--- name: aisa-multi-search-engine description: 'Multi-source search engine powered by AIsa API. Combines Tavily web search, Scholar academic search, Smart hybrid search, and Perplexity deep research — all through a single AIsa API key. Includes confidence scoring and AI synthesis. Use when: the user needs web search, research, source discovery, or content extraction.' author: AIsa version: 1.0.2 license: MIT user-invocable: true primaryEnv: AISA_API_KEY requires: bins: - python3 - node env: - AISA_API_KEY metadata: aisa: emoji: 🔎 requires: bins: - python3 - node env: - AISA_API_KEY primaryEnv: AISA_API_KEY compatibility: - openclaw - claude-code - hermes openclaw: emoji: 🔎 requires: bins: - python3 - node env: - AISA_API_KEY primaryEnv: AISA_API_KEY --- # AIsa Multi Search Engine v1.0.0 Multi-source search engine powered by [AIsa API](https://aisa.one). One API key unlocks web search, academic search, Tavily integration, and Perplexity deep research with confidence-scored results. ## Quick Start ```bash export AISA_API_KEY="your-key-here" ``` Get your API key at [aisa.one](https://aisa.one). ## Search Tools This plugin registers seven agent tools, each backed by a different AIsa API endpoint. | Tool | Description | AIsa Endpoint | |------|-------------|---------------| | `aisa_web_search` | Structured web search results | `/scholar/search/web` | | `aisa_scholar_search` | Academic paper search with year filtering | `/scholar/search/scholar` | | `aisa_smart_search` | Intelligent hybrid search (web + academic) | `/scholar/search/smart` | | `aisa_tavily_search` | Advanced web search with depth, topic, time, domain filters | `/tavily/search` | | `aisa_tavily_extract` | Extract clean content from URLs | `/tavily/extract` | | `aisa_perplexity_search` | Deep research via Perplexity Sonar models | `/sonar`, `/sonar-pro`, etc. | | `aisa_multi_search` | Parallel multi-source search with confidence scoring | Multiple endpoints | ## Usage Examples ### Web Search Search the web for structured results. ``` Use aisa_web_search to find "latest developments in AI agents 2025" ``` ### Academic Search Search scholarly papers with optional year filtering. ``` Use aisa_scholar_search to find papers on "transformer architecture" from 2024 to 2025 ``` ### Smart Search Intelligent hybrid search combining web and academic sources. ``` Use aisa_smart_search to research "autonomous AI agents" ``` ### Tavily Search (Advanced) Advanced web search with filtering options. ``` Use aisa_tavily_search for "AI startup funding" with topic "news" and time_range "month" ``` ### Tavily Extract Extract full content from specific URLs. ``` Use aisa_tavily_extract to get content from ["https://example.com/article"] ``` ### Perplexity Deep Research Use Perplexity Sonar models for synthesized answers with citations. ``` Use aisa_perplexity_search to answer "What is the current state of quantum computing?" with model "sonar-pro" ``` | Model | Best For | |-------|----------| | `sonar` | Quick answers, fast response | | `sonar-pro` | Detailed answers with more citations | | `sonar-reasoning-pro` | Complex reasoning and analysis | | `sonar-deep-research` | Exhaustive multi-step research | ### Multi-Source Search (Verity-Style) Parallel search across all sources with confidence scoring and AI synthesis. ``` Use aisa_multi_search to comprehensively research "Is quantum computing ready for enterprise use?" ``` ## Confidence Scoring The `aisa_multi_search` tool provides a deterministic confidence score based on four factors. | Factor | Weight | Description | |--------|--------|-------------| | Source Availability | 40% | How many search sources returned results | | Result Quality | 35% | Volume of results relative to expectations | | Source Diversity | 15% | Mix of academic and web sources | | Recency | 10% | Bonus for having any successful sources | | Score | Level | Interpretation | |-------|-------|----------------| | 90-100 | Very High | Strong consensus across sources | | 70-89 | High | Good agreement, reliable sources | | 50-69 | Medium | Mixed signals, verify independently | | 30-49 | Low | Conflicting or sparse sources | | 0-29 | Very Low | Insufficient data | ## Python CLI Client A standalone Python client is included for command-line use. ```bash # Web search python3 scripts/search_client.py web --query "latest AI news" --count 10 # Academic search python3 scripts/search_client.py scholar --query "transformer architecture" --year-from 2024 # Smart search python3 scripts/search_client.py smart --query "autonomous agents" --count 10 # Tavily search python3 scripts/search_client.py tavily --query "AI developments" --depth advanced # Extract content from URLs python3 scripts/search_client.py extract --urls "https://example.com/article" # Perplexity deep research python3 scripts/search_client.py sonar --query "quantum computing" --model sonar-pro # Multi-source search with confidence scoring python3 scripts/search_client.py verity --query "Is quantum computing ready?" ``` ## API Endpoints Reference All endpoints use the AIsa API base URL `https://api.aisa.one/apis/v1`. | Endpoint | Method | Description | |----------|--------|-------------| | `/scholar/search/web` | POST | Web search with structured results | | `/scholar/search/scholar` | POST | Academic paper search | | `/scholar/search/smart` | POST | Intelligent hybrid search | | `/scholar/explain` | POST | Generate result explanations with confidence | | `/tavily/search` | POST | Tavily web search | | `/tavily/extract` | POST | Extract content from URLs | | `/tavily/crawl` | POST | Crawl web pages | | `/tavily/map` | POST | Generate site maps | | `/sonar` | POST | Perplexity Sonar (fast) | | `/sonar-pro` | POST | Perplexity Sonar Pro (detailed) | | `/sonar-reasoning-pro` | POST | Perplexity Sonar Reasoning Pro | | `/sonar-deep-research` | POST | Perplexity Sonar Deep Research | ## Configuration The plugin accepts the following configuration options. | Option | Type | Default | Description | |--------|------|---------|-------------| | `aisaApiKey` | string | — | AIsa API key (or set `AISA_API_KEY` env var) | | `defaultMaxResults` | integer | 10 | Default max results per search (1-100) | | `defaultSearchDepth` | string | `basic` | Default Tavily search depth | ## Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ User Query │ └─────────────────────────────────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Scholar │ │ Web │ │ Smart │ └─────────┘ └─────────┘ └─────────┘ │ │ │ └───────────────┼───────────────┘ ▼ ┌─────────────────┐ │ Tavily Search │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ AIsa Explain │ │ (Meta-Analysis) │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Confidence Score│ │ + Synthesis │ └─────────────────┘ ``` ## Pricing | API | Approximate Cost | |-----|-----------------| | Web search | ~$0.001 | | Scholar search | ~$0.002 | | Smart search | ~$0.002 | | Tavily search | ~$0.002 | | Explain | ~$0.003 | | Perplexity Sonar | ~$0.005 | ## Resources - [AIsa](https://aisa.one) — Get your API key - [AIsa API Reference](https://aisa.one/docs/api-reference) — Complete endpoint documentation - [ClawHub](https://clawhub.ai) — Plugin marketplace ## License MIT

path: README.md
content: # AIsa Multi Search Engine

Release-ready ClawHub skill package generated from `targetSkills/aisa-multi-search-engine`.

- Skill name: `aisa-multi-search-engine`
- Entry point: `SKILL.md`
- Runtime assets: `scripts/`, `references/`, `assets/` when present

## Notes

- Multi-source search engine powered by AIsa API. Combines Tavily web search, Scholar academic search, Smart hybrid search, and Perplexity deep research — all through a single AIsa API key. Includes confidence scoring and AI synthesis. Use when: the user needs web search, research, source discovery, or content extraction.
- This README is release-specific and replaces source READMEs that were written for other runtimes.
- If the underlying instructions mention OpenClaw, treat that as source-context or compatibility guidance unless the skill is specifically about OpenClaw setup.

## Quick Start

1. Open `SKILL.md` to review invocation guidance and runtime requirements.
2. Set any required environment variables before running bundled scripts.
3. Use repo-relative paths like `python3 scripts/...` when following command examples.


path: index.ts
content: // index.ts — AIsa Multi Search Engine Plugin for OpenClaw
// Powered by AIsa API (https://docs.aisa.one)
//
// Provides multi-source search tools:
//   - aisa_web_search        — Web search via AIsa Scholar Web endpoint
//   - aisa_scholar_search    — Academic paper search via AIsa Scholar endpoint
//   - aisa_smart_search      — Intelligent hybrid search (web + academic)
//   - aisa_tavily_search     — Tavily web search with advanced filtering
//   - aisa_tavily_extract    — Extract content from URLs via Tavily
//   - aisa_perplexity_search — Deep research via Perplexity Sonar models
//   - aisa_multi_search      — Parallel multi-source search with confidence scoring

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const AISA_BASE = "https://api.aisa.one/apis/v1";

interface PluginConfig {
  aisaApiKey?: string;
  defaultMaxResults?: number;
  defaultSearchDepth?: string;
}

function getApiKey(config: PluginConfig): string {
  const key = config.aisaApiKey || process.env.AISA_API_KEY || "";
  if (!key) {
    throw new Error(
      "AISA_API_KEY is not configured. Set it in plugin config or as an environment variable. " +
        "Get your key at https://aisa.one",
    );
  }
  return key;
}

async function aisaPost(
  [REDACTED_SECRET],
  path: string,
  body: Record<string, unknown>,
): Promise<unknown> {
  const url = `${AISA_BASE}${path}`;
  const res = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`AIsa API error ${res.status} on ${path}: ${text}`);
  }
  return res.json();
}

async function aisaGet(
  [REDACTED_SECRET],
  path: string,
  params: Record<string, string>,
): Promise<unknown> {
  const qs = new URLSearchParams(params).toString();
  const url = `${AISA_BASE}${path}${qs ? `?${qs}` : ""}`;
  const res = await fetch(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      Accept: "application/json",
    },
  });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`AIsa API error ${res.status} on ${path}: ${text}`);
  }
  return res.json();
}

function formatResults(data: unknown, source: string): string {
  const obj = data as Record<string, unknown>;
  const lines: string[] = [`## ${source} Results\n`];

  // Handle Tavily-style results
  if (Array.isArray(obj.results)) {
    for (const r of obj.results as Record<string, unknown>[]) {
      lines.push(`### ${r.title || "Untitled"}`);
      if (r.url) lines.push(`**URL:** ${r.url}`);
      if (r.published_date) lines.push(`**Date:** ${r.published_date}`);
      if (r.content) lines.push(`\n${r.content}\n`);
      else if (r.snippet) lines.push(`\n${r.snippet}\n`);
      lines.push("---");
    }
  }

  // Handle Scholar-style results with organic_results
  if (Array.isArray(obj.organic_results)) {
    for (const r of obj.organic_results as Record<string, unknown>[]) {
      lines.push(`### ${r.title || "Untitled"}`);
      if (r.link) lines.push(`**URL:** ${r.link}`);
      if (r.publication_info)
        lines.push(
          `**Publication:** ${JSON.stringify(r.publication_info)}`,
        );
      if (r.snippet) lines.push(`\n${r.snippet}\n`);
      lines.push("---");
    }
  }

  // Handle answer field (Tavily include_answer or Perplexity)
  if (obj.answer) {
    lines.push(`\n**Answer:** ${obj.answer}\n`);
  }

  // Handle Perplexity-style choices
  if (Array.isArray(obj.choices)) {
    for (const c of obj.choices as Record<string, unknown>[]) {
      const msg = c.message as Record<string, unknown> | undefined;
      if (msg?.content) {
        lines.push(`\n${msg.content}\n`);
      }
    }
  }

  // Handle citations
  if (Array.isArray(obj.citations)) {
    lines.push("\n**Citations:**");
    for (const cite of obj.citations as string[]) {
      lines.push(`- ${cite}`);
    }
  }

  // Handle usage/cost info
  if (obj.usage) {
    const usage = obj.usage as Record<string, unknown>;
    if (usage.cost !== undefined) {
      lines.push(`\n*Cost: $${usage.cost}*`);
    }
  }

  if (lines.length <= 1) {
    lines.push("No results found.");
    lines.push(`\nRaw response:\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``);
  }

  return lines.join("\n");
}

// ---------------------------------------------------------------------------
// Plugin Entry
// ---------------------------------------------------------------------------

export default definePluginEntry({
  id: "aisa-multi-search-engine",
  name: "AIsa Multi Search Engine",
  description:
    "Multi-source search powered by AIsa API — web, academic, Tavily, and Perplexity in one plugin.",

  register(api) {
    const config = (api as unknown as { config: PluginConfig }).config ?? {};

    // -----------------------------------------------------------------------
    // Tool 1: Web Search (AIsa Scholar Web)
    // -----------------------------------------------------------------------
    api.registerTool({
      name: "aisa_web_search",
      description:
        "Search the web using AIsa Scholar Web endpoint. Returns structured web results with titles, URLs, and snippets.",
      parameters: Type.Object({
        query: Type.String({ description: "Search query" }),
        max_results: Type.Optional(
          Type.Integer({
            description: "Maximum results (1-100, default 10)",
            minimum: 1,
            maximum: 100,
          }),
        ),
      }),
      async execute(_id, params) {
        const [REDACTED_SECRET]);
        const maxResults =
          params.max_results ?? config.defaultMaxResults ?? 10;
        const data = await aisaPost(apiKey, "/scholar/search/web", {
          query: params.query,
          max_num_results: maxResults,
        });
        const text = formatResults(data, "Web Search");
        return { content: [{ type: "text", text }] };
      },
    });

    // -----------------------------------------------------------------------
    // Tool 2: Scholar Search (Academic Papers)
    // -----------------------------------------------------------------------
    api.registerTool({
      name: "aisa_scholar_search",
      description:
        "Search academic papers and scholarly articles via AIsa Scholar endpoint. Supports year range filtering.",
      parameters: Type.Object({
        query: Type.String({ description: "Academic search query" }),
        max_results: Type.Optional(
          Type.Integer({
            description: "Maximum results (1-100, default 10)",
            minimum: 1,
            maximum: 100,
          }),
        ),
        year_from: Type.Optional(
          Type.Integer({ description: "Year lower bound (e.g. 2023)" }),
        ),
        year_to: Type.Optional(
          Type.Integer({ description: "Year upper bound (e.g. 2025)" }),
        ),
      }),
      async execute(_id, params) {
        const [REDACTED_SECRET]);
        const maxResults =
          params.max_results ?? config.defaultMaxResults ?? 10;
        const body: Record<string, unknown> = {
          query: params.query,
          max_num_results: maxResults,
        };
        if (params.year_from) body.as_ylo = params.year_from;
        if (params.year_to) body.as_yhi = params.year_to;
        const data = await aisaPost(apiKey, "/scholar/search/scholar", body);
        const text = formatResults(data, "Scholar Search");
        return { content: [{ type: "text", text }] };
      },
    });

    // -----------------------------------------------------------------------
    // Tool 3: Smart Search (Hybrid Web + Academic)
    // -----------------------------------------------------------------------
    api.registerTool({
      name: "aisa_smart_search",
      description:
        "Intelligent hybrid search combining web and academic sources via AIsa Smart Search endpoint.",
      parameters: Type.Object({
        query: Type.String({ description: "Search query" }),
        max_results: Type.Optional(
          Type.Integer({
            description: "Maximum results (1-100, default 10)",
            minimum: 1,
            maximum: 100,
          }),
        ),
      }),
      async execute(_id, params) {
        const [REDACTED_SECRET]);
        const maxResults =
          params.max_results ?? config.defaultMaxResults ?? 10;
        const data = await aisaPost(apiKey, "/scholar/search/smart", {
          query: params.query,
          max_num_results: maxResults,
        });
        const text = formatResults(data, "Smart Search");
        return { content: [{ type: "text", text }] };
      },
    });

    // -----------------------------------------------------------------------
    // Tool 4: Tavily Search (Advanced Web Search)
    // -----------------------------------------------------------------------
    api.registerTool({
      name: "aisa_tavily_search",
      description:
        "Advanced web search via Tavily through AIsa API. Supports search depth, topic filtering, time ranges, domain inclusion/exclusion, and LLM-generated answers.",
      parameters: Type.Object({
        query: Type.String({ description: "Search query" }),
        search_depth: Type.Optional(
          Type.String({
            description: "Search depth: basic, advanced, fast, or ultra-fast",
            enum: ["basic", "advanced", "fast", "ultra-fast"],
          }),
        ),
        max_results: Type.Optional(
          Type.Integer({
            description: "Maximum results (1-20, default 5)",
            minimum: 1,
            maximum: 20,
          }),
        ),
        topic: Type.Optional(
          Type.String({
            description: "Topic category: general, news, or finance",
            enum: ["general", "news", "finance"],
          }),
        ),
        time_range: Type.Optional(
          Type.String({
            description:
              "Time range filter: day, week, month, year, or d/w/m/y shorthand",
          }),
        ),
        include_answer: Type.Optional(
          Type.Boolean({
            description: "Include an LLM-generated answer (default false)",
          }),
        ),
        include_domains: Type.Optional(
          Type.Array(Type.String(), {
            description: "Domains to include in results",
          }),
        ),
        exclude_domains: Type.Optional(
          Type.Array(Type.String(), {
            description: "Domains to exclude from results",
          }),
        ),
        country: Type.Optional(
          Type.String({
            description:
              "Boost results from a specific country (ISO 3166-1 alpha-2 code)",
          }),
        ),
      }),
      async execute(_id, params) {
        const [REDACTED_SECRET]);
        const body: Record<string, unknown> = {
          query: params.query,
          search_depth:
            params.search_depth ?? config.defaultSearchDepth ?? "basic",
          max_results: params.max_results ?? 5,
        };
        if (params.topic) body.topic = params.topic;
        if (params.time_range) body.time_range = params.time_range;
        if (params.include_answer !== undefined)
          body.include_answer = params.include_answer;
        if (params.include_domains)
          body.include_domains = params.include_domains;
        if (params.exclude_domains)
          body.exclude_domains = params.exclude_domains;
        if (params.country) body.country = params.country;

        const data = await aisaPost(apiKey, "/tavily/search", body);
        const text = formatResults(data, "Tavily Search");
        return { content: [{ type: "text", text }] };
      },
    });

    // -----------------------------------------------------------------------
    // Tool 5: Tavily Extract (Content Extraction from URLs)
    // -----------------------------------------------------------------------
    api.registerTool({
      name: "aisa_tavily_extract",
      description:
        "Extract clean content from one or more URLs using Tavily Extract via AIsa API. Useful for reading full articles.",
      parameters: Type.Object({
        urls: Type.Array(Type.String(), {
          description: "List of URLs to extract content from",
        }),
      }),
      async execute(_id, params) {
        const [REDACTED_SECRET]);
        const data = await aisaPost(apiKey, "/tavily/extract", {
          urls: params.urls,
        });
        const obj = data as Record<string, unknown>;
        const lines: string[] = ["## Tavily Extract Results\n"];
        if (Array.isArray(obj.results)) {
          for (const r of obj.results as Record<string, unknown>[]) {
            lines.push(`### ${r.url || "Unknown URL"}`);
            if (r.raw_content)
              lines.push(
                `\n${(r.raw_content as string).slice(0, 3000)}\n`,
              );
            lines.push("---");
          }
        } else {
          lines.push(
            `\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``,
          );
        }
        return { content: [{ type: "text", text: lines.join("\n") }] };
      },
    });

    // -----------------------------------------------------------------------
    // Tool 6: Perplexity Search (Deep Research)
    // -----------------------------------------------------------------------
    api.registerTool({
      name: "aisa_perplexity_search",
      description:
        "Deep research using Perplexity Sonar models via AIsa API. Provides synthesized answers with citations. Models: sonar (fast), sonar-pro (detailed), sonar-reasoning-pro (complex reasoning), sonar-deep-research (exhaustive).",
      parameters: Type.Object({
        query: Type.String({ description: "Research question or query" }),
        model: Type.Optional(
          Type.String({
            description:
              "Perplexity model: sonar, sonar-pro, sonar-reasoning-pro, or sonar-deep-research",
            enum: [
              "sonar",
              "sonar-pro",
              "sonar-reasoning-pro",
              "sonar-deep-research",
            ],
          }),
        ),
      }),
      async execute(_id, params) {
        const [REDACTED_SECRET]);
        const model = params.model ?? "sonar";

        // Map model names to AIsa API endpoints
        const endpointMap: Record<string, string> = {
          sonar: "/sonar",
          "sonar-pro": "/sonar-pro",
          "sonar-reasoning-pro": "/sonar-reasoning-pro",
          "sonar-deep-research": "/sonar-deep-research",
        };
        const endpoint = endpointMap[model] ?? "/sonar";

        const data = await aisaPost(apiKey, endpoint, {
          model,
          messages: [{ role: "user", content: params.query }],
        });
        const text = formatResults(data, `Perplexity (${model})`);
        return { content: [{ type: "text", text }] };
      },
    });

    // -----------------------------------------------------------------------
    // Tool 7: Multi Search (Parallel multi-source with confidence scoring)
    // -----------------------------------------------------------------------
    api.registerTool({
      name: "aisa_multi_search",
      description:
        "Parallel multi-source search combining web, scholar, smart, and Tavily results with confidence scoring. Best for comprehensive research requiring cross-source validation.",
      parameters: Type.Object({
        query: Type.String({ description: "Search query" }),
        max_results: Type.Optional(
          Type.Integer({
            description: "Maximum results per source (1-20, default 5)",
            minimum: 1,
            maximum: 20,
          }),
        ),
        include_scholar: Type.Optional(
          Type.Boolean({
            description: "Include academic/scholar results (default true)",
          }),
        ),
        include_tavily: Type.Optional(
          Type.Boolean({
            description: "Include Tavily results (default true)",
          }),
        ),
        include_explanation: Type.Optional(
          Type.Boolean({
            description:
              "Include AIsa Explain meta-analysis with confidence scoring (default true)",
          }),
        ),
      }),
      async execute(_id, params) {
        const [REDACTED_SECRET]);
        const maxResults = params.max_results ?? 5;
        const includeScholar = params.include_scholar !== false;
        const includeTavily = params.include_tavily !== false;
        const includeExplanation = params.include_explanation !== false;

        // Phase 1: Parallel retrieval
        const tasks: Promise<{ source: string; data: unknown }>[] = [];

        // Always include web search
        tasks.push(
          aisaPost(apiKey, "/scholar/search/web", {
            query: params.query,
            max_num_results: maxResults,
          }).then((data) => ({ source: "Web", data })),
        );

        // Always include smart search
        tasks.push(
          aisaPost(apiKey, "/scholar/search/smart", {
            query: params.query,
            max_num_results: maxResults,
          }).then((data) => ({ source: "Smart", data })),
        );

        if (includeScholar) {
          tasks.push(
            aisaPost(apiKey, "/scholar/search/scholar", {
              query: params.query,
              max_num_results: maxResults,
            }).then((data) => ({ source: "Scholar", data })),
          );
        }

        if (includeTavily) {
          tasks.push(
            aisaPost(apiKey, "/tavily/search", {
              query: params.query,
              max_results: maxResults,
              include_answer: true,
            }).then((data) => ({ source: "Tavily", data })),
          );
        }

        const results = await Promise.allSettled(tasks);
        const sections: string[] = [
          `# Multi-Source Search: "${params.query}"\n`,
        ];
        const successfulResults: Record<string, unknown>[] = [];

        for (const result of results) {
          if (result.status === "fulfilled") {
            const { source, data } = result.value;
            sections.push(formatResults(data, source));
            successfulResults.push(data as Record<string, unknown>);
          } else {
            sections.push(`\n> Search source failed: ${result.reason}\n`);
          }
        }

        // Phase 2: Confidence scoring (deterministic)
        const totalSources = results.filter(
          (r) => r.status === "fulfilled",
        ).length;
        let confidenceScore = 0;

        // Source availability (40%)
        confidenceScore += (totalSources / tasks.length) * 40;

        // Result count quality (35%)
        let totalResults = 0;
        for (const d of successfulResults) {
          if (Array.isArray(d.results)) totalResults += d.results.length;
          if (Array.isArray(d.organic_results))
            totalResults += d.organic_results.length;
        }
        const expectedResults = maxResults * tasks.length;
        confidenceScore +=
          Math.min(totalResults / expectedResults, 1) * 35;

        // Source diversity (15%)
        const hasScholar = successfulResults.some((d) =>
          Array.isArray(d.organic_results),
        );
        const hasWeb = successfulResults.some(
          (d) =>
            Array.isArray(d.results) &&
            !Array.isArray(d.organic_results),
        );
        const diversityScore =
          (hasScholar ? 0.5 : 0) + (hasWeb ? 0.5 : 0);
        confidenceScore += diversityScore * 15;

        // Recency bonus (10%)
        confidenceScore += totalSources > 0 ? 10 : 0;

        confidenceScore = Math.min(Math.round(confidenceScore), 100);

        // Confidence interpretation
        let confidenceLevel: string;
        if (confidenceScore >= 90) confidenceLevel = "Very High";
        else if (confidenceScore >= 70) confidenceLevel = "High";
        else if (confidenceScore >= 50) confidenceLevel = "Medium";
        else if (confidenceScore >= 30) confidenceLevel = "Low";
        else confidenceLevel = "Very Low";

        sections.push(`\n## Confidence Assessment`);
        sections.push(`- **Score:** ${confidenceScore}/100`);
        sections.push(`- **Level:** ${confidenceLevel}`);
        sections.push(
          `- **Sources queried:** ${tasks.length} | **Successful:** ${totalSources}`,
        );
        sections.push(`- **Total results:** ${totalResults}`);

        // Phase 3: Optional explanation via AIsa Explain
        if (includeExplanation && successfulResults.length > 0) {
          try {
            const explainData = await aisaPost(
              apiKey,
              "/scholar/explain",
              {
                results: successfulResults,
                language: "en",
                format: "summary",
              },
            );
            const explainObj = explainData as Record<string, unknown>;
            if (explainObj.summary) {
              sections.push(`\n## AI Synthesis\n`);
              sections.push(explainObj.summary as string);
            }
            if (Array.isArray(explainObj.citations)) {
              sections.push(`\n**Citations:**`);
              for (const c of explainObj.citations as string[]) {
                sections.push(`- ${c}`);
              }
            }
          } catch {
            sections.push(
              `\n> *Explanation generation skipped (API error).*`,
            );
          }
        }

        return { content: [{ type: "text", text: sections.join("\n") }] };
      },
    });
  },
});


path: package.json
content: {
  "name": "aisa-multi-search-engine",
  "version": "1.0.0",
  "description": "Multi-source search engine powered by AIsa API. Combines Tavily, Scholar, Smart search, and Perplexity — all through a single AIsa API key.",
  "type": "module",
  "license": "MIT",
  "keywords": [
    "openclaw",
    "plugin",
    "search",
    "aisa",
    "tavily",
    "scholar",
    "perplexity",
    "web-search",
    "multi-search"
  ],
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": {
      "pluginApi": ">=2026.3.24",
      "minGatewayVersion": "2026.3.24"
    },
    "build": {
      "openclawVersion": "2026.3.28",
      "pluginSdkVersion": "2026.3.28"
    }
  },
  "dependencies": {
    "@sinclair/typebox": "^0.32.0"
  }
}


path: scripts/search_client.py
content: #!/usr/bin/env python3
"""
AIsa Multi Search Engine — Python CLI Client
=============================================

A command-line search client that calls AIsa API endpoints for multi-source
search.  Requires the AISA_API_KEY environment variable.

Usage:
    python3 search_client.py web      --query "latest AI news" --count 10
    python3 search_client.py scholar  --query "transformer architecture" --year-from 2024
    python3 search_client.py smart    --query "autonomous agents" --count 10
    python3 search_client.py tavily   --query "AI developments" --depth advanced
    python3 search_client.py extract  --urls "https://example.com/article"
    python3 search_client.py sonar    --query "quantum computing" --model sonar-pro
    python3 search_client.py verity   --query "Is quantum computing ready?"
"""

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any

AISA_BASE = "https://api.aisa.one/apis/v1"


def get_api_key() -> str:
    key = os.environ.get("AISA_API_KEY", "")
    if not key:
        print("Error: AISA_API_KEY environment variable is not set.", file=sys.stderr)
        print("Get your key at https://aisa.one", file=sys.stderr)
        sys.exit(1)
    return key


def aisa_post(api_key: str, path: str, body: dict[str, Any]) -> dict[str, Any]:
    url = f"{AISA_BASE}{path}"
    data = json.dumps(body).encode()
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            return json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        body_text = e.read().decode() if e.fp else ""
        print(f"API error {e.code} on {path}: {body_text}", file=sys.stderr)
        return {"error": str(e), "status": e.code}


def print_results(data: dict[str, Any], source: str) -> None:
    print(f"\n{'='*60}")
    print(f"  {source} Results")
    print(f"{'='*60}\n")

    if "results" in data and isinstance(data["results"], list):
        for i, r in enumerate(data["results"], 1):
            print(f"  [{i}] {r.get('title', 'Untitled')}")
            if r.get("url"):
                print(f"      URL: {r['url']}")
            if r.get("published_date"):
                print(f"      Date: {r['published_date']}")
            snippet = r.get("content") or r.get("snippet") or ""
            if snippet:
                print(f"      {snippet[:200]}")
            print()

    if "organic_results" in data and isinstance(data["organic_results"], list):
        for i, r in enumerate(data["organic_results"], 1):
            print(f"  [{i}] {r.get('title', 'Untitled')}")
            if r.get("link"):
                print(f"      URL: {r['link']}")
            if r.get("snippet"):
                print(f"      {r['snippet'][:200]}")
            print()

    if "answer" in data:
        print(f"  Answer: {data['answer']}\n")

    if "choices" in data and isinstance(data["choices"], list):
        for c in data["choices"]:
            msg = c.get("message", {})
            if msg.get("content"):
                print(f"  {msg['content'][:1000]}\n")

    if "citations" in data and isinstance(data["citations"], list):
        print("  Citations:")
        for cite in data["citations"]:
            print(f"    - {cite}")
        print()

    if "usage" in data:
        usage = data["usage"]
        if "cost" in usage:
            print(f"  Cost: ${usage['cost']}")
        if "credits_remaining" in usage:
            print(f"  Credits remaining: {usage['credits_remaining']}")
        print()


def cmd_web(args: argparse.Namespace) -> None:
    [REDACTED_SECRET])
    data = aisa_post(api_key, "/scholar/search/web", {
        "query": args.query,
        "max_num_results": args.count,
    })
    print_results(data, "Web Search")


def cmd_scholar(args: argparse.Namespace) -> None:
    [REDACTED_SECRET])
    body: dict[str, Any] = {
        "query": args.query,
        "max_num_results": args.count,
    }
    if args.year_from:
        body["as_ylo"] = args.year_from
    if args.year_to:
        body["as_yhi"] = args.year_to
    data = aisa_post(api_key, "/scholar/search/scholar", body)
    print_results(data, "Scholar Search")


def cmd_smart(args: argparse.Namespace) -> None:
    [REDACTED_SECRET])
    data = aisa_post(api_key, "/scholar/search/smart", {
        "query": args.query,
        "max_num_results": args.count,
    })
    print_results(data, "Smart Search")


def cmd_tavily(args: argparse.Namespace) -> None:
    [REDACTED_SECRET])
    body: dict[str, Any] = {
        "query": args.query,
        "max_results": args.count,
        "search_depth": args.depth,
    }
    if args.topic:
        body["topic"] = args.topic
    if args.time_range:
        body["time_range"] = args.time_range
    if args.include_answer:
        body["include_answer"] = True
    data = aisa_post(api_key, "/tavily/search", body)
    print_results(data, "Tavily Search")


def cmd_extract(args: argparse.Namespace) -> None:
    [REDACTED_SECRET])
    urls = [u.strip() for u in args.urls.split(",")]
    data = aisa_post(api_key, "/tavily/extract", {"urls": urls})
    if "results" in data and isinstance(data["results"], list):
        for r in data["results"]:
            print(f"\n{'='*60}")
            print(f"  URL: {r.get('url', 'Unknown')}")
            print(f"{'='*60}")
            content = r.get("raw_content", "")
            print(content[:3000] if content else "(no content)")
    else:
        print(json.dumps(data, indent=2))


def cmd_sonar(args: argparse.Namespace) -> None:
    [REDACTED_SECRET])
    endpoint_map = {
        "sonar": "/sonar",
        "sonar-pro": "/sonar-pro",
        "sonar-reasoning-pro": "/sonar-reasoning-pro",
        "sonar-deep-research": "/sonar-deep-research",
    }
    endpoint = endpoint_map.get(args.model, "/sonar")
    data = aisa_post(api_key, endpoint, {
        "model": args.model,
        "messages": [{"role": "user", "content": args.query}],
    })
    print_results(data, f"Perplexity ({args.model})")


def cmd_verity(args: argparse.Namespace) -> None:
    """Multi-source search with confidence scoring."""
    [REDACTED_SECRET])
    count = args.count

    print(f"\nSearching across multiple sources for: \"{args.query}\"\n")

    # Phase 1: Parallel retrieval
    sources: dict[str, dict[str, Any]] = {}
    tasks = {
        "Web": ("/scholar/search/web", {"query": args.query, "max_num_results": count}),
        "Smart": ("/scholar/search/smart", {"query": args.query, "max_num_results": count}),
        "Scholar": ("/scholar/search/scholar", {"query": args.query, "max_num_results": count}),
        "Tavily": ("/tavily/search", {"query": args.query, "max_results": count, "include_answer": True}),
    }

    def fetch(name: str, endpoint: str, body: dict[str, Any]) -> tuple[str, dict[str, Any]]:
        return name, aisa_post(api_key, endpoint, body)

    with ThreadPoolExecutor(max_workers=4) as pool:
        futures = {
            pool.submit(fetch, name, ep, body): name
            for name, (ep, body) in tasks.items()
        }
        for future in as_completed(futures):
            name = futures[future]
            try:
                _, data = future.result()
                sources[name] = data
                print(f"  [OK] {name}")
            except Exception as e:
                print(f"  [FAIL] {name}: {e}")

    # Print individual results
    for name, data in sources.items():
        print_results(data, name)

    # Phase 2: Confidence scoring
    total_sources = len(sources)
    total_results = 0
    for d in sources.values():
        if isinstance(d.get("results"), list):
            total_results += len(d["results"])
        if isinstance(d.get("organic_results"), list):
            total_results += len(d["organic_results"])

    score = 0.0
    score += (total_sources / len(tasks)) * 40
    expected = count * len(tasks)
    score += min(total_results / expected, 1.0) * 35
    has_scholar = any(isinstance(d.get("organic_results"), list) for d in sources.values())
    has_web = any(
        isinstance(d.get("results"), list) and not isinstance(d.get("organic_results"), list)
        for d in sources.values()
    )
    score += ((0.5 if has_scholar else 0) + (0.5 if has_web else 0)) * 15
    score += 10 if total_sources > 0 else 0
    score = min(round(score), 100)

    levels = {90: "Very High", 70: "High", 50: "Medium", 30: "Low", 0: "Very Low"}
    level = "Very Low"
    for threshold in sorted(levels.keys(), reverse=True):
        if score >= threshold:
            level = levels[threshold]
            break

    print(f"\n{'='*60}")
    print(f"  Confidence Assessment")
    print(f"{'='*60}")
    print(f"  Score:            {score}/100")
    print(f"  Level:            {level}")
    print(f"  Sources queried:  {len(tasks)}")
    print(f"  Sources OK:       {total_sources}")
    print(f"  Total results:    {total_results}")

    # Phase 3: Explanation
    if sources:
        print(f"\n  Generating synthesis...")
        explain_data = aisa_post(api_key, "/scholar/explain", {
            "results": list(sources.values()),
            "language": "en",
            "format": "summary",
        })
        if "summary" in explain_data:
            print(f"\n  AI Synthesis:")
            print(f"  {explain_data['summary']}")
        if isinstance(explain_data.get("citations"), list):
            print(f"\n  Citations:")
            for c in explain_data["citations"]:
                print(f"    - {c}")
    print()


def main() -> None:
    parser = argparse.ArgumentParser(
        description="AIsa Multi Search Engine CLI",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    sub = parser.add_subparsers(dest="command", required=True)

    # web
    p_web = sub.add_parser("web", help="Web search")
    p_web.add_argument("--query", "-q", required=True)
    p_web.add_argument("--count", "-c", type=int, default=10)
    p_web.set_defaults(func=cmd_web)

    # scholar
    p_scholar = sub.add_parser("scholar", help="Academic paper search")
    p_scholar.add_argument("--query", "-q", required=True)
    p_scholar.add_argument("--count", "-c", type=int, default=10)
    p_scholar.add_argument("--year-from", type=int, default=None)
    p_scholar.add_argument("--year-to", type=int, default=None)
    p_scholar.set_defaults(func=cmd_scholar)

    # smart
    p_smart = sub.add_parser("smart", help="Smart hybrid search")
    p_smart.add_argument("--query", "-q", required=True)
    p_smart.add_argument("--count", "-c", type=int, default=10)
    p_smart.set_defaults(func=cmd_smart)

    # tavily
    p_tavily = sub.add_parser("tavily", help="Tavily web search")
    p_tavily.add_argument("--query", "-q", required=True)
    p_tavily.add_argument("--count", "-c", type=int, default=5)
    p_tavily.add_argument("--depth", default="basic", choices=["basic", "advanced", "fast", "ultra-fast"])
    p_tavily.add_argument("--topic", default=None, choices=["general", "news", "finance"])
    p_tavily.add_argument("--time-range", default=None)
    p_tavily.add_argument("--include-answer", action="store_true")
    p_tavily.set_defaults(func=cmd_tavily)

    # extract
    p_extract = sub.add_parser("extract", help="Extract content from URLs")
    p_extract.add_argument("--urls", "-u", required=True, help="Comma-separated URLs")
    p_extract.set_defaults(func=cmd_extract)

    # sonar (perplexity)
    p_sonar = sub.add_parser("sonar", help="Perplexity deep research")
    p_sonar.add_argument("--query", "-q", required=True)
    p_sonar.add_argument(
        "--model", "-m", default="sonar",
        choices=["sonar", "sonar-pro", "sonar-reasoning-pro", "sonar-deep-research"],
    )
    p_sonar.set_defaults(func=cmd_sonar)

    # verity (multi-source)
    p_verity = sub.add_parser("verity", help="Multi-source search with confidence scoring")
    p_verity.add_argument("--query", "-q", required=True)
    p_verity.add_argument("--count", "-c", type=int, default=5)
    p_verity.set_defaults(func=cmd_verity)

    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()


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