From 3bcf1fedd4769661e37c06fe450ad2dca846a3c4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 4 Feb 2025 19:45:50 -0800 Subject: [PATCH] Added serper web search tool/block --- blocks/blocks/serper.ts | 100 +++++++++++++++++++++++++++ blocks/index.ts | 7 +- tools/index.ts | 5 +- tools/serper/search.ts | 150 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 blocks/blocks/serper.ts create mode 100644 tools/serper/search.ts diff --git a/blocks/blocks/serper.ts b/blocks/blocks/serper.ts new file mode 100644 index 000000000..b9f060313 --- /dev/null +++ b/blocks/blocks/serper.ts @@ -0,0 +1,100 @@ +import { BlockConfig } from '../types' +import { SearchIcon } from '@/components/icons' +import { SearchResponse } from '@/tools/serper/search' + +export const SerperBlock: BlockConfig = { + type: 'serper_search', + toolbar: { + title: 'Web Search', + description: 'Search the web', + bgColor: '#4285F4', // Google blue + icon: SearchIcon, + category: 'tools', + }, + tools: { + access: ['serper_search'] + }, + workflow: { + inputs: { + query: { type: 'string', required: true }, + apiKey: { type: 'string', required: true }, + num: { type: 'number', required: false }, + gl: { type: 'string', required: false }, + hl: { type: 'string', required: false }, + type: { type: 'string', required: false } + }, + outputs: { + response: { + type: { + searchResults: 'json' + } + } + }, + subBlocks: [ + { + id: 'query', + title: 'Search Query', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your search query...' + }, + { + id: 'type', + title: 'Search Type', + type: 'dropdown', + layout: 'half', + options: ['search', 'news', 'places', 'images'] + }, + { + id: 'num', + title: 'Number of Results', + type: 'dropdown', + layout: 'half', + options: ['10', '20', '30', '40', '50', '100'] + }, + { + id: 'gl', + title: 'Country', + type: 'dropdown', + layout: 'half', + options: [ + 'US', + 'GB', + 'CA', + 'AU', + 'DE', + 'FR', + 'ES', + 'IT', + 'JP', + 'KR' + ] + }, + { + id: 'hl', + title: 'Language', + type: 'dropdown', + layout: 'half', + options: [ + 'en', + 'es', + 'fr', + 'de', + 'it', + 'pt', + 'ja', + 'ko', + 'zh' + ] + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your Serper API key', + password: true + } + ] + } +} \ No newline at end of file diff --git a/blocks/index.ts b/blocks/index.ts index 808d9bc33..a07fed864 100644 --- a/blocks/index.ts +++ b/blocks/index.ts @@ -11,6 +11,7 @@ import { TranslateBlock } from './blocks/translate' import { SlackMessageBlock } from './blocks/slack' import { GitHubBlock } from './blocks/github' import { ConditionBlock } from './blocks/condition' +import { SerperBlock } from './blocks/serper' // Export blocks for ease of use export { @@ -23,7 +24,8 @@ export { TranslateBlock, SlackMessageBlock, GitHubBlock, - ConditionBlock + ConditionBlock, + SerperBlock } // Registry of all block configurations @@ -37,7 +39,8 @@ const blocks: Record = { translate: TranslateBlock, slack_message: SlackMessageBlock, github_repo_info: GitHubBlock, - condition: ConditionBlock + condition: ConditionBlock, + serper_search: SerperBlock } // Build a reverse mapping of tools to block types diff --git a/tools/index.ts b/tools/index.ts index 74ffedd96..aacb10761 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -14,6 +14,7 @@ import { scrapeTool } from './firecrawl/scrape' import { readUrlTool } from './jina/reader' import { slackMessageTool } from './slack/message' import { repoInfoTool } from './github/repo' +import { searchTool as serperSearch } from './serper/search' // Registry of all available tools export const tools: Record = { @@ -40,7 +41,9 @@ export const tools: Record = { // Slack Tools 'slack_message': slackMessageTool, // GitHub Tools - 'github_repoinfo': repoInfoTool + 'github_repoinfo': repoInfoTool, + // Search Tools + 'serper_search': serperSearch } // Get a tool by its ID diff --git a/tools/serper/search.ts b/tools/serper/search.ts new file mode 100644 index 000000000..141bfd7a1 --- /dev/null +++ b/tools/serper/search.ts @@ -0,0 +1,150 @@ +import { ToolConfig, ToolResponse } from '../types' + +interface SearchParams { + query: string + apiKey: string + num?: number + gl?: string // country code + hl?: string // language code + type?: 'search' | 'news' | 'places' | 'images' +} + +export interface SearchResult { + title: string + link: string + snippet: string + position: number + imageUrl?: string + date?: string + rating?: string + reviews?: string + address?: string +} + +export interface SearchResponse extends ToolResponse { + output: { + searchResults: SearchResult[] + } +} + +export const searchTool: ToolConfig = { + id: 'serper_search', + name: 'Web Search', + description: 'Search the web using Serper.dev API', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: true, + description: 'The search query' + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Serper API Key' + }, + num: { + type: 'number', + required: false, + description: 'Number of results to return' + }, + gl: { + type: 'string', + required: false, + description: 'Country code for search results' + }, + hl: { + type: 'string', + required: false, + description: 'Language code for search results' + }, + type: { + type: 'string', + required: false, + description: 'Type of search to perform' + } + }, + + request: { + url: (params) => `https://google.serper.dev/${params.type || 'search'}`, + method: 'POST', + headers: (params) => ({ + 'X-API-KEY': params.apiKey, + 'Content-Type': 'application/json' + }), + body: (params) => { + const body: Record = { + q: params.query + } + + // Only include optional parameters if they are explicitly set + if (params.num) body.num = params.num + if (params.gl) body.gl = params.gl + if (params.hl) body.hl = params.hl + + return body + } + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || 'Failed to perform search') + } + + const searchType = response.url.split('/').pop() || 'search' + let searchResults: SearchResult[] = [] + + if (searchType === 'news') { + searchResults = data.news?.map((item: any, index: number) => ({ + title: item.title, + link: item.link, + snippet: item.snippet, + position: index + 1, + date: item.date, + imageUrl: item.imageUrl + })) || [] + } else if (searchType === 'places') { + searchResults = data.places?.map((item: any, index: number) => ({ + title: item.title, + link: item.link, + snippet: item.snippet, + position: index + 1, + rating: item.rating, + reviews: item.reviews, + address: item.address + })) || [] + } else if (searchType === 'images') { + searchResults = data.images?.map((item: any, index: number) => ({ + title: item.title, + link: item.link, + snippet: item.snippet, + position: index + 1, + imageUrl: item.imageUrl + })) || [] + } else { + searchResults = data.organic?.map((item: any, index: number) => ({ + title: item.title, + link: item.link, + snippet: item.snippet, + position: index + 1 + })) || [] + } + + return { + success: true, + output: { + searchResults + } + } + }, + + transformError: (error) => { + return error instanceof Error + ? error.message + : 'An error occurred while performing the search' + } +} \ No newline at end of file