All files / src/web-search index.ts

51.78% Statements 58/112
81.81% Branches 9/11
50% Functions 4/8
51.78% Lines 58/112

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177                        1x                                                                               1x 1x 1x 1x 1x       1x 5x 5x 2x   5x 2x 2x 2x 2x 2x   3x 3x 5x 5x   5x                                         5x                                 5x                                   5x 5x 5x 5x 5x   5x 5x           5x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x   5x 3x 3x 5x 5x   1x 1x 1x 1x 1x 1x 1x  
/**
 * AgentKits — Web Search Module
 *
 * Multi-provider web search with unified interface.
 * Supports: Tavily, SerpAPI, Brave Search.
 *
 * Usage:
 *   import { createWebSearch } from 'agentkits/web-search';
 *   const search = createWebSearch({ provider: 'tavily', apiKey: '...' });
 *   const results = await search.search('latest AI news');
 */
 
import { ValidationError, ProviderError } from '../errors/index.js';
 
// ── Types ──────────────────────────────────────────────────────────
 
export type SearchProvider = 'tavily' | 'serpapi' | 'brave';
 
export interface SearchConfig {
  provider?: SearchProvider;
  apiKey?: string;
  maxResults?: number;
}
 
export interface SearchResult {
  title: string;
  url: string;
  snippet: string;
  score?: number;
}
 
export interface SearchResponse {
  results: SearchResult[];
  query: string;
  provider: SearchProvider;
}
 
export interface SearchClient {
  /** Search the web */
  search(query: string, options?: { maxResults?: number }): Promise<SearchResponse>;
  /** Get as function-calling tool definition */
  readonly toolDefinition: { type: 'function'; function: { name: string; description: string; parameters: Record<string, unknown> } };
  readonly config: Readonly<ResolvedSearchConfig>;
}
 
interface ResolvedSearchConfig {
  provider: SearchProvider;
  maxResults: number;
}
 
// ── ENV resolution ────────────────────────────────────────────────
 
const ENV_MAP: Record<SearchProvider, string[]> = {
  tavily:  ['TAVILY_API_KEY'],
  serpapi: ['SERPAPI_API_KEY'],
  brave:   ['BRAVE_API_KEY'],
};
 
// ── Factory ────────────────────────────────────────────────────────
 
export function createWebSearch(userConfig: SearchConfig = {}): SearchClient {
  const provider = userConfig.provider ?? 'tavily';
  const apiKey = userConfig.apiKey
    ?? (ENV_MAP[provider] ?? []).map(k => process.env[k]).find(Boolean);
 
  if (!apiKey) {
    throw new ValidationError(
      `Missing API key for search provider "${provider}". Set ${ENV_MAP[provider].join(' or ')}.`,
      { field: 'apiKey' },
    );
  }
 
  const resolved: ResolvedSearchConfig = {
    provider,
    maxResults: userConfig.maxResults ?? 5,
  };
 
  async function searchTavily(query: string, maxResults: number): Promise<SearchResult[]> {
    const res = await fetch('https://api.tavily.com/search', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        api_key: apiKey,
        query,
        max_results: maxResults,
        include_answer: false,
      }),
    });
    if (!res.ok) throw new ProviderError(`Tavily error: ${res.status}`, { provider: 'tavily', statusCode: res.status });
    const data = await res.json() as any;
    return (data.results ?? []).map((r: any) => ({
      title: r.title ?? '',
      url: r.url ?? '',
      snippet: r.content ?? '',
      score: r.score,
    }));
  }
 
  async function searchSerpApi(query: string, maxResults: number): Promise<SearchResult[]> {
    const params = new URLSearchParams({
      q: query,
      api_key: apiKey!,
      engine: 'google',
      num: String(maxResults),
    });
    const res = await fetch(`https://serpapi.com/search?${params}`);
    if (!res.ok) throw new ProviderError(`SerpAPI error: ${res.status}`, { provider: 'serpapi', statusCode: res.status });
    const data = await res.json() as any;
    return (data.organic_results ?? []).slice(0, maxResults).map((r: any) => ({
      title: r.title ?? '',
      url: r.link ?? '',
      snippet: r.snippet ?? '',
    }));
  }
 
  async function searchBrave(query: string, maxResults: number): Promise<SearchResult[]> {
    const params = new URLSearchParams({ q: query, count: String(maxResults) });
    const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
      headers: {
        'Accept': 'application/json',
        'Accept-Encoding': 'gzip',
        'X-Subscription-Token': apiKey!,
      },
    });
    if (!res.ok) throw new ProviderError(`Brave Search error: ${res.status}`, { provider: 'brave', statusCode: res.status });
    const data = await res.json() as any;
    return (data.web?.results ?? []).slice(0, maxResults).map((r: any) => ({
      title: r.title ?? '',
      url: r.url ?? '',
      snippet: r.description ?? '',
    }));
  }
 
  const searchFns: Record<SearchProvider, (q: string, n: number) => Promise<SearchResult[]>> = {
    tavily: searchTavily,
    serpapi: searchSerpApi,
    brave: searchBrave,
  };
 
  return {
    async search(query, options = {}) {
      const maxResults = options.maxResults ?? resolved.maxResults;
      const results = await searchFns[provider](query, maxResults);
      return { results, query, provider };
    },
 
    get toolDefinition() {
      return {
        type: 'function' as const,
        function: {
          name: 'web_search',
          description: 'Search the web for current information',
          parameters: {
            type: 'object',
            properties: {
              query: { type: 'string', description: 'Search query' },
            },
            required: ['query'],
          },
        },
      };
    },
 
    get config() {
      return resolved;
    },
  };
}
 
export function listSearchProviders(): Array<{ id: SearchProvider; region: string }> {
  return [
    { id: 'tavily',  region: 'Global' },
    { id: 'serpapi', region: 'Global' },
    { id: 'brave',   region: 'Global' },
  ];
}