feat: added exa ai blocks/tools

This commit is contained in:
Waleed Latif
2025-03-02 14:15:17 -08:00
parent 4771541d27
commit 12c2566f30
9 changed files with 685 additions and 15 deletions
+210
View File
@@ -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<ExaResponse> = {
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',
},
},
},
}
+17 -14
View File
@@ -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<string, BlockConfig> = {
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
+20 -1
View File
File diff suppressed because one or more lines are too long
+73
View File
@@ -0,0 +1,73 @@
import { ToolConfig } from '../types'
import { ExaAnswerParams, ExaAnswerResponse } from './types'
export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
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<string, any> = {
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'
},
}
+87
View File
@@ -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<string, any> = {
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'
},
}
+89
View File
@@ -0,0 +1,89 @@
import { ToolConfig } from '../types'
import { ExaGetContentsParams, ExaGetContentsResponse } from './types'
export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsResponse> = {
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<string, any> = {
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'
},
}
+91
View File
@@ -0,0 +1,91 @@
import { ToolConfig } from '../types'
import { ExaSearchParams, ExaSearchResponse } from './types'
export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
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<string, any> = {
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'
},
}
+90
View File
@@ -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
}[]
}
}
+8
View File
@@ -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<string, ToolConfig> = {
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