diff --git a/blocks/blocks/serper.ts b/blocks/blocks/serper.ts index b9f0603132..7a45a1222b 100644 --- a/blocks/blocks/serper.ts +++ b/blocks/blocks/serper.ts @@ -1,5 +1,5 @@ import { BlockConfig } from '../types' -import { SearchIcon } from '@/components/icons' +import { SerperIcon } from '@/components/icons' import { SearchResponse } from '@/tools/serper/search' export const SerperBlock: BlockConfig = { @@ -8,7 +8,7 @@ export const SerperBlock: BlockConfig = { title: 'Web Search', description: 'Search the web', bgColor: '#4285F4', // Google blue - icon: SearchIcon, + icon: SerperIcon, category: 'tools', }, tools: { diff --git a/blocks/blocks/tavily.ts b/blocks/blocks/tavily.ts new file mode 100644 index 0000000000..d82232e134 --- /dev/null +++ b/blocks/blocks/tavily.ts @@ -0,0 +1,112 @@ +import { BlockConfig } from '../types' +import { TavilyIcon } from '@/components/icons' +import { SearchResponse } from '@/tools/tavily/search' +import { ExtractResponse } from '@/tools/tavily/extract' + +export const TavilySearchBlock: BlockConfig = { + type: 'tavily_search', + toolbar: { + title: 'Tavily Search', + description: 'Search the web using Tavily AI', + bgColor: '#0066FF', + icon: TavilyIcon, + category: 'tools', + }, + tools: { + access: ['tavily_search'] + }, + workflow: { + inputs: { + query: { type: 'string', required: true }, + apiKey: { type: 'string', required: true }, + max_results: { type: 'number', required: false } + }, + outputs: { + response: { + type: { + query: 'string', + results: 'json', + response_time: 'number' + } + } + }, + subBlocks: [ + { + id: 'query', + title: 'Search Query', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your search query...' + }, + { + id: 'max_results', + title: 'Max Results', + type: 'dropdown', + layout: 'half', + options: ['5', '10', '15', '20'] + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your Tavily API key', + password: true + } + ] + } +} + +export const TavilyExtractBlock: BlockConfig = { + type: 'tavily_extract', + toolbar: { + title: 'Tavily Extract', + description: 'Scrape website content', + bgColor: '#0066FF', + icon: TavilyIcon, + category: 'tools', + }, + tools: { + access: ['tavily_extract'] + }, + workflow: { + inputs: { + urls: { type: 'string', required: true }, + apiKey: { type: 'string', required: true }, + extract_depth: { type: 'string', required: false } + }, + outputs: { + response: { + type: { + results: 'json', + failed_results: 'any', + response_time: 'number' + } + } + }, + subBlocks: [ + { + id: 'urls', + title: 'URL', + type: 'short-input', + layout: 'full', + placeholder: 'Enter URL to extract content from...' + }, + { + id: 'extract_depth', + title: 'Extract Depth', + type: 'dropdown', + layout: 'half', + options: ['basic', 'advanced'] + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your Tavily API key', + password: true + } + ] + } +} \ No newline at end of file diff --git a/blocks/index.ts b/blocks/index.ts index a07fed864e..093dda30b4 100644 --- a/blocks/index.ts +++ b/blocks/index.ts @@ -12,6 +12,7 @@ import { SlackMessageBlock } from './blocks/slack' import { GitHubBlock } from './blocks/github' import { ConditionBlock } from './blocks/condition' import { SerperBlock } from './blocks/serper' +import { TavilySearchBlock, TavilyExtractBlock } from './blocks/tavily' // Export blocks for ease of use export { @@ -25,7 +26,9 @@ export { SlackMessageBlock, GitHubBlock, ConditionBlock, - SerperBlock + SerperBlock, + TavilySearchBlock, + TavilyExtractBlock } // Registry of all block configurations @@ -40,7 +43,9 @@ const blocks: Record = { slack_message: SlackMessageBlock, github_repo_info: GitHubBlock, condition: ConditionBlock, - serper_search: SerperBlock + serper_search: SerperBlock, + tavily_search: TavilySearchBlock, + tavily_extract: TavilyExtractBlock } // Build a reverse mapping of tools to block types diff --git a/components/icons.tsx b/components/icons.tsx index 2a51973473..7cf8cb99c8 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -1016,3 +1016,17 @@ export const GithubIcon = (props: SVGProps) => ( /> ) + +export const SerperIcon = (props: SVGProps) => ( + + + + +) + +export const TavilyIcon = (props: SVGProps) => ( + + + + +) diff --git a/tools/index.ts b/tools/index.ts index aacb107615..c65acbdbc9 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -15,6 +15,8 @@ import { readUrlTool } from './jina/reader' import { slackMessageTool } from './slack/message' import { repoInfoTool } from './github/repo' import { searchTool as serperSearch } from './serper/search' +import { searchTool as tavilySearch } from './tavily/search' +import { extractTool as tavilyExtract } from './tavily/extract' // Registry of all available tools export const tools: Record = { @@ -43,7 +45,9 @@ export const tools: Record = { // GitHub Tools 'github_repoinfo': repoInfoTool, // Search Tools - 'serper_search': serperSearch + 'serper_search': serperSearch, + 'tavily_search': tavilySearch, + 'tavily_extract': tavilyExtract } // Get a tool by its ID diff --git a/tools/tavily/extract.ts b/tools/tavily/extract.ts new file mode 100644 index 0000000000..7a2db60754 --- /dev/null +++ b/tools/tavily/extract.ts @@ -0,0 +1,90 @@ +import { ToolConfig, ToolResponse } from '../types' + +interface ExtractParams { + urls: string | string[] + apiKey: string + extract_depth?: 'basic' | 'advanced' +} + +interface ExtractResult { + url: string + raw_content: string +} + +export interface ExtractResponse extends ToolResponse { + output: { + results: ExtractResult[] + failed_results?: Array<{ + url: string + error: string + }> + response_time: number + } +} + +export const extractTool: ToolConfig = { + id: 'tavily.extract', + name: 'Tavily Extract', + description: 'Extract web page content from URLs using Tavily Extract', + version: '1.0.0', + + params: { + urls: { + type: 'string', + required: true, + description: 'URL or array of URLs to extract content from' + }, + apiKey: { + type: 'string', + required: true, + description: 'Tavily API Key', + requiredForToolCall: true + }, + extract_depth: { + type: 'string', + required: false, + description: 'The depth of extraction (basic=1 credit/5 URLs, advanced=2 credits/5 URLs)' + } + }, + + request: { + url: 'https://api.tavily.com/extract', + method: 'POST', + headers: (params) => ({ + 'Authorization': `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json' + }), + body: (params) => { + const body: Record = { + urls: typeof params.urls === 'string' ? [params.urls] : params.urls + } + + if (params.extract_depth) body.extract_depth = params.extract_depth + + return body + } + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || 'Failed to extract content') + } + + return { + success: true, + output: { + results: data.results || [], + ...(data.failed_results && { failed_results: data.failed_results }), + response_time: data.response_time + } + } + }, + + transformError: (error) => { + return error instanceof Error + ? error.message + : 'An error occurred while extracting content' + } +} \ No newline at end of file diff --git a/tools/tavily/search.ts b/tools/tavily/search.ts new file mode 100644 index 0000000000..3582ff5aaa --- /dev/null +++ b/tools/tavily/search.ts @@ -0,0 +1,95 @@ +import { ToolConfig, ToolResponse } from '../types' + +interface SearchParams { + query: string + apiKey: string + max_results?: number +} + +interface SearchResult { + title: string + url: string + snippet: string + raw_content?: string +} + +export interface SearchResponse extends ToolResponse { + output: { + query: string + results: SearchResult[] + response_time: number + } +} + +export const searchTool: ToolConfig = { + id: 'tavily_search', + name: 'Tavily Search', + description: 'Search the web using Tavily AI Search API', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: true, + description: 'The search query to execute' + }, + max_results: { + type: 'number', + required: false, + description: 'Maximum number of results (1-20)' + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Tavily API Key' + } + }, + + request: { + url: 'https://api.tavily.com/search', + method: 'POST', + headers: (params) => ({ + 'Authorization': `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json' + }), + body: (params) => { + const body: Record = { + query: params.query + } + + // Only include optional parameters if explicitly set + if (params.max_results) body.max_results = params.max_results + + return body + } + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || 'Failed to perform search') + } + + return { + success: true, + output: { + query: data.query, + results: data.results.map((result: any) => ({ + title: result.title, + url: result.url, + snippet: result.snippet, + ...(result.raw_content && { raw_content: result.raw_content }) + })), + response_time: data.response_time + } + } + }, + + transformError: (error) => { + return error instanceof Error + ? error.message + : 'An error occurred while performing the search' + } +} \ No newline at end of file