From 12c2566f30296adbd4a3ac56631b6f8ec81fdb91 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 2 Mar 2025 14:15:15 -0800 Subject: [PATCH] feat: added exa ai blocks/tools --- blocks/blocks/exa.ts | 210 ++++++++++++++++++++++++++++++++++ blocks/index.ts | 31 ++--- components/icons.tsx | 21 +++- tools/exa/answer.ts | 73 ++++++++++++ tools/exa/findSimilarLinks.ts | 87 ++++++++++++++ tools/exa/getContents.ts | 89 ++++++++++++++ tools/exa/search.ts | 91 +++++++++++++++ tools/exa/types.ts | 90 +++++++++++++++ tools/index.ts | 8 ++ 9 files changed, 685 insertions(+), 15 deletions(-) create mode 100644 blocks/blocks/exa.ts create mode 100644 tools/exa/answer.ts create mode 100644 tools/exa/findSimilarLinks.ts create mode 100644 tools/exa/getContents.ts create mode 100644 tools/exa/search.ts create mode 100644 tools/exa/types.ts diff --git a/blocks/blocks/exa.ts b/blocks/blocks/exa.ts new file mode 100644 index 0000000000..6c55a43367 --- /dev/null +++ b/blocks/blocks/exa.ts @@ -0,0 +1,210 @@ +import { ExaAIIcon } from '@/components/icons' +import { + ExaAnswerResponse, + ExaFindSimilarLinksResponse, + ExaGetContentsResponse, + ExaSearchResponse, +} from '@/tools/exa/types' +import { BlockConfig } from '../types' + +type ExaResponse = + | ExaSearchResponse + | ExaGetContentsResponse + | ExaFindSimilarLinksResponse + | ExaAnswerResponse + +export const ExaBlock: BlockConfig = { + type: 'exa', + name: 'Exa', + description: 'Search with Exa AI', + category: 'tools', + bgColor: '#1F41ED', + icon: ExaAIIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Search', id: 'exa_search' }, + { label: 'Get Contents', id: 'exa_get_contents' }, + { label: 'Find Similar Links', id: 'exa_find_similar_links' }, + { label: 'Answer', id: 'exa_answer' }, + ], + value: () => 'exa_search', + }, + // API Key (common) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your Exa API key', + password: true, + }, + // Search operation inputs + { + id: 'query', + title: 'Search Query', + type: 'long-input', + layout: 'full', + placeholder: 'Enter your search query...', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'numResults', + title: 'Number of Results', + type: 'short-input', + layout: 'full', + placeholder: '10', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'useAutoprompt', + title: 'Use Autoprompt', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'type', + title: 'Search Type', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Auto', id: 'auto' }, + { label: 'Neural', id: 'neural' }, + { label: 'Keyword', id: 'keyword' }, + { label: 'Magic', id: 'magic' }, + ], + value: () => 'auto', + condition: { field: 'operation', value: 'exa_search' }, + }, + // Get Contents operation inputs + { + id: 'urls', + title: 'URLs', + type: 'long-input', + layout: 'full', + placeholder: 'Enter URLs to retrieve content from (comma-separated)...', + condition: { field: 'operation', value: 'exa_get_contents' }, + }, + { + id: 'text', + title: 'Include Text', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_get_contents' }, + }, + { + id: 'summaryQuery', + title: 'Summary Query', + type: 'long-input', + layout: 'full', + placeholder: 'Optional: Enter a query to guide the summary generation...', + condition: { field: 'operation', value: 'exa_get_contents' }, + }, + // Find Similar Links operation inputs + { + id: 'url', + title: 'URL', + type: 'long-input', + layout: 'full', + placeholder: 'Enter URL to find similar links for...', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'numResults', + title: 'Number of Results', + type: 'short-input', + layout: 'full', + placeholder: '10', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'text', + title: 'Include Text', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + // Answer operation inputs + { + id: 'query', + title: 'Question', + type: 'long-input', + layout: 'full', + placeholder: 'Enter your question...', + condition: { field: 'operation', value: 'exa_answer' }, + }, + { + id: 'text', + title: 'Include Text', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_answer' }, + }, + ], + tools: { + access: ['exa_search', 'exa_get_contents', 'exa_find_similar_links', 'exa_answer'], + config: { + tool: (params) => { + // Parse comma-separated URLs for Get Contents operation + if (params.operation === 'exa_get_contents' && params.urls) { + params.urls = params.urls + .split(',') + .map((url: string) => url.trim()) + .filter((url: string) => url.length > 0) + } + + // Convert numResults to a number for operations that use it + if (params.numResults) { + params.numResults = Number(params.numResults) + } + + switch (params.operation) { + case 'exa_search': + return 'exa_search' + case 'exa_get_contents': + return 'exa_get_contents' + case 'exa_find_similar_links': + return 'exa_find_similar_links' + case 'exa_answer': + return 'exa_answer' + default: + return 'exa_search' + } + }, + }, + }, + inputs: { + operation: { type: 'string', required: true }, + apiKey: { type: 'string', required: true }, + // Search operation + query: { type: 'string', required: false }, + numResults: { type: 'number', required: false }, + useAutoprompt: { type: 'boolean', required: false }, + type: { type: 'string', required: false }, + // Get Contents operation + urls: { type: 'string', required: false }, + text: { type: 'boolean', required: false }, + summaryQuery: { type: 'string', required: false }, + // Find Similar Links operation + url: { type: 'string', required: false }, + }, + outputs: { + response: { + type: { + // Search output + results: 'json', + // Find Similar Links output + similarLinks: 'json', + // Answer output + answer: 'string', + citations: 'json', + }, + }, + }, +} diff --git a/blocks/index.ts b/blocks/index.ts index bb1bc25658..0b0843dbc9 100644 --- a/blocks/index.ts +++ b/blocks/index.ts @@ -4,6 +4,7 @@ import { ApiBlock } from './blocks/api' import { ConditionBlock } from './blocks/condition' import { CrewAIVisionBlock } from './blocks/crewai' import { EvaluatorBlock } from './blocks/evaluator' +import { ExaBlock } from './blocks/exa' import { FirecrawlBlock } from './blocks/firecrawl' import { FunctionBlock } from './blocks/function' import { GitHubBlock } from './blocks/github' @@ -45,31 +46,33 @@ export { StarterBlock, PineconeBlock, OpenAIBlock, + ExaBlock, } -// Registry of all block configurations +// Registry of all block configurations, alphabetically sorted const blocks: Record = { agent: AgentBlock, api: ApiBlock, - starter: StarterBlock, condition: ConditionBlock, - function: FunctionBlock, - router: RouterBlock, - evaluator: EvaluatorBlock, crewai_vision: CrewAIVisionBlock, + evaluator: EvaluatorBlock, + exa: ExaBlock, firecrawl: FirecrawlBlock, - jina: JinaBlock, - translate: TranslateBlock, - slack: SlackBlock, + function: FunctionBlock, github: GitHubBlock, - serper: SerperBlock, - tavily: TavilyBlock, - youtube: YouTubeBlock, - notion: NotionBlock, gmail: GmailBlock, - x: XBlock, - pinecone: PineconeBlock, + jina: JinaBlock, + notion: NotionBlock, openai: OpenAIBlock, + pinecone: PineconeBlock, + router: RouterBlock, + serper: SerperBlock, + slack: SlackBlock, + starter: StarterBlock, + tavily: TavilyBlock, + translate: TranslateBlock, + x: XBlock, + youtube: YouTubeBlock, } // Helper functions diff --git a/components/icons.tsx b/components/icons.tsx index 28c492ff2e..457fb5ddb9 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -1336,7 +1336,7 @@ export function BrainIcon(props: SVGProps) { strokeLinejoin="round" /> ) { ) } + +export function ExaAIIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/tools/exa/answer.ts b/tools/exa/answer.ts new file mode 100644 index 0000000000..7c760bdb00 --- /dev/null +++ b/tools/exa/answer.ts @@ -0,0 +1,73 @@ +import { ToolConfig } from '../types' +import { ExaAnswerParams, ExaAnswerResponse } from './types' + +export const answerTool: ToolConfig = { + id: 'exa_answer', + name: 'Exa Answer', + description: 'Get an AI-generated answer to a question with citations from the web using Exa AI.', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: true, + description: 'The question to answer', + }, + text: { + type: 'boolean', + required: false, + description: 'Whether to include the full text of the answer', + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Exa AI API Key', + }, + }, + + request: { + url: 'https://api.exa.ai/answer', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-api-key': params.apiKey, + }), + body: (params) => { + const body: Record = { + query: params.query, + } + + // Add optional parameters if provided + if (params.text) body.text = params.text + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || data.error || 'Failed to generate answer') + } + + return { + success: true, + output: { + query: data.query || '', + answer: data.answer || '', + citations: + data.citations?.map((citation: any) => ({ + title: citation.title || '', + url: citation.url, + text: citation.text || '', + })) || [], + }, + } + }, + + transformError: (error) => { + return error instanceof Error ? error.message : 'An error occurred while generating an answer' + }, +} diff --git a/tools/exa/findSimilarLinks.ts b/tools/exa/findSimilarLinks.ts new file mode 100644 index 0000000000..ff41078a60 --- /dev/null +++ b/tools/exa/findSimilarLinks.ts @@ -0,0 +1,87 @@ +import { ToolConfig } from '../types' +import { ExaFindSimilarLinksParams, ExaFindSimilarLinksResponse } from './types' + +export const findSimilarLinksTool: ToolConfig< + ExaFindSimilarLinksParams, + ExaFindSimilarLinksResponse +> = { + id: 'exa_find_similar_links', + name: 'Exa Find Similar Links', + description: + 'Find webpages similar to a given URL using Exa AI. Returns a list of similar links with titles and text snippets.', + version: '1.0.0', + + params: { + url: { + type: 'string', + required: true, + description: 'The URL to find similar links for', + }, + numResults: { + type: 'number', + required: false, + description: 'Number of similar links to return (default: 10, max: 25)', + }, + text: { + type: 'boolean', + required: false, + description: 'Whether to include the full text of the similar pages', + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Exa AI API Key', + }, + }, + + request: { + url: 'https://api.exa.ai/findSimilar', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-api-key': params.apiKey, + }), + body: (params) => { + const body: Record = { + url: params.url, + } + + // Add optional parameters if provided + if (params.numResults) body.numResults = params.numResults + + // Add contents.text parameter if text is true + if (params.text) { + body.contents = { + text: true, + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || data.error || 'Failed to find similar links') + } + + return { + success: true, + output: { + similarLinks: data.results.map((result: any) => ({ + title: result.title || '', + url: result.url, + text: result.text || '', + score: result.score || 0, + })), + }, + } + }, + + transformError: (error) => { + return error instanceof Error ? error.message : 'An error occurred while finding similar links' + }, +} diff --git a/tools/exa/getContents.ts b/tools/exa/getContents.ts new file mode 100644 index 0000000000..f33a7a349b --- /dev/null +++ b/tools/exa/getContents.ts @@ -0,0 +1,89 @@ +import { ToolConfig } from '../types' +import { ExaGetContentsParams, ExaGetContentsResponse } from './types' + +export const getContentsTool: ToolConfig = { + id: 'exa_get_contents', + name: 'Exa Get Contents', + description: + 'Retrieve the contents of webpages using Exa AI. Returns the title, text content, and optional summaries for each URL.', + version: '1.0.0', + + params: { + urls: { + type: 'string[]', + required: true, + description: 'Array of URLs to retrieve content from', + }, + text: { + type: 'boolean', + required: false, + description: + 'If true, returns full page text with default settings. If false, disables text return.', + }, + summaryQuery: { + type: 'string', + required: false, + description: 'Query to guide the summary generation', + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Exa AI API Key', + }, + }, + + request: { + url: 'https://api.exa.ai/contents', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-api-key': params.apiKey, + }), + body: (params) => { + const body: Record = { + urls: params.urls, + } + + // Add optional parameters if provided + if (params.text !== undefined) { + body.text = params.text + } + + // Add summary with query if provided + if (params.summaryQuery) { + body.summary = { + query: params.summaryQuery, + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || data.error || 'Failed to retrieve webpage contents') + } + + return { + success: true, + output: { + results: data.results.map((result: any) => ({ + url: result.url, + title: result.title || '', + text: result.text || '', + summary: result.summary || '', + })), + }, + } + }, + + transformError: (error) => { + return error instanceof Error + ? error.message + : 'An error occurred while retrieving webpage contents' + }, +} diff --git a/tools/exa/search.ts b/tools/exa/search.ts new file mode 100644 index 0000000000..7ce70329fb --- /dev/null +++ b/tools/exa/search.ts @@ -0,0 +1,91 @@ +import { ToolConfig } from '../types' +import { ExaSearchParams, ExaSearchResponse } from './types' + +export const searchTool: ToolConfig = { + id: 'exa_search', + name: 'Exa Search', + description: + 'Search the web using Exa AI. Returns relevant search results with titles, URLs, and text snippets.', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: true, + description: 'The search query to execute', + }, + numResults: { + type: 'number', + required: false, + description: 'Number of results to return (default: 10, max: 25)', + }, + useAutoprompt: { + type: 'boolean', + required: false, + description: 'Whether to use autoprompt to improve the query (default: false)', + }, + type: { + type: 'string', + required: false, + description: 'Search type: neural, keyword, auto or magic (default: auto)', + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Exa AI API Key', + }, + }, + + request: { + url: 'https://api.exa.ai/search', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-api-key': params.apiKey, + }), + body: (params) => { + const body: Record = { + query: params.query, + } + + // Add optional parameters if provided + if (params.numResults) body.numResults = params.numResults + if (params.useAutoprompt !== undefined) body.useAutoprompt = params.useAutoprompt + if (params.type) body.type = params.type + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || data.error || 'Failed to perform Exa search') + } + + return { + success: true, + output: { + results: data.results.map((result: any) => ({ + title: result.title || '', + url: result.url, + publishedDate: result.publishedDate, + author: result.author, + summary: result.summary, + favicon: result.favicon, + image: result.image, + text: result.text, + score: result.score, + })), + }, + } + }, + + transformError: (error) => { + return error instanceof Error + ? error.message + : 'An error occurred while performing the Exa search' + }, +} diff --git a/tools/exa/types.ts b/tools/exa/types.ts new file mode 100644 index 0000000000..dcdbeb4568 --- /dev/null +++ b/tools/exa/types.ts @@ -0,0 +1,90 @@ +// Common types for Exa AI tools +import { ToolResponse } from '../types' + +// Common parameters for all Exa AI tools +export interface ExaBaseParams { + apiKey: string +} + +// Search tool types +export interface ExaSearchParams extends ExaBaseParams { + query: string + numResults?: number + useAutoprompt?: boolean + type?: 'auto' | 'neural' | 'keyword' | 'magic' +} + +export interface ExaSearchResult { + title: string + url: string + publishedDate?: string + author?: string + summary?: string + favicon?: string + image?: string + text: string + score: number +} + +export interface ExaSearchResponse extends ToolResponse { + output: { + results: ExaSearchResult[] + } +} + +// Get Contents tool types +export interface ExaGetContentsParams extends ExaBaseParams { + urls: string[] + text?: boolean + summaryQuery?: string +} + +export interface ExaGetContentsResult { + url: string + title: string + text: string + summary?: string +} + +export interface ExaGetContentsResponse extends ToolResponse { + output: { + results: ExaGetContentsResult[] + } +} + +// Find Similar Links tool types +export interface ExaFindSimilarLinksParams extends ExaBaseParams { + url: string + numResults?: number + text?: boolean +} + +export interface ExaSimilarLink { + title: string + url: string + text: string + score: number +} + +export interface ExaFindSimilarLinksResponse extends ToolResponse { + output: { + similarLinks: ExaSimilarLink[] + } +} + +// Answer tool types +export interface ExaAnswerParams extends ExaBaseParams { + query: string + text?: boolean +} + +export interface ExaAnswerResponse extends ToolResponse { + output: { + answer: string + citations: { + title: string + url: string + text: string + }[] + } +} diff --git a/tools/index.ts b/tools/index.ts index 86a33e5cd4..a57e6324ef 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -1,6 +1,10 @@ import { useCustomToolsStore } from '@/stores/custom-tools/store' import { useEnvironmentStore } from '@/stores/settings/environment/store' import { visionTool as crewAIVision } from './crewai/vision' +import { answerTool as exaAnswer } from './exa/answer' +import { findSimilarLinksTool as exaFindSimilarLinks } from './exa/findSimilarLinks' +import { getContentsTool as exaGetContents } from './exa/getContents' +import { searchTool as exaSearch } from './exa/search' import { scrapeTool } from './firecrawl/scrape' import { functionExecuteTool as functionExecute } from './function/execute' import { webcontainerExecuteTool as webcontainerExecute } from './function/webcontainer' @@ -67,6 +71,10 @@ export const tools: Record = { pinecone_upsert_text: pineconeUpsertTextTool, github_pr: prTool, github_comment: commentTool, + exa_search: exaSearch, + exa_get_contents: exaGetContents, + exa_find_similar_links: exaFindSimilarLinks, + exa_answer: exaAnswer, } // Get a tool by its ID