Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ BETTER_AUTH_SECRET=****
BETTER_AUTH_URL=

# (Optional)
# === Tools ===
# === Tools ===
# Exa AI for web search and content extraction (optional, but recommended for @web and research features)
EXA_API_KEY=

# Tavily AI for web search and content extraction (alternative to Exa)
TAVILY_API_KEY=

# Search provider to use: 'exa' (default) or 'tavily'
SEARCH_PROVIDER=



# ========================================================================
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# dependencies
/node_modules
package-lock.json
/.pnp
.pnp.*
.yarn/*
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.8",
"@tavily/core": "^0.6.4",
"@tiptap/extension-mention": "^2.27.1",
"@tiptap/react": "^2.27.1",
"@tiptap/starter-kit": "^2.27.1",
Expand Down Expand Up @@ -162,7 +163,10 @@
"vitest": "^3.2.4"
},
"lint-staged": {
"*.{js,json,mjs,ts,yaml,tsx,css}": ["pnpm format", "pnpm lint:fix"]
"*.{js,json,mjs,ts,yaml,tsx,css}": [
"pnpm format",
"pnpm lint:fix"
]
},
"packageManager": "pnpm@10.2.1",
"engines": {
Expand Down
8 changes: 5 additions & 3 deletions src/lib/ai/tools/tool-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { createPieChartTool } from "./visualization/create-pie-chart";
import { createBarChartTool } from "./visualization/create-bar-chart";
import { createLineChartTool } from "./visualization/create-line-chart";
import { createTableTool } from "./visualization/create-table";
import { exaSearchTool, exaContentsTool } from "./web/web-search";
import { getWebSearchTools } from "./web/web-search";
import { AppDefaultToolkit, DefaultToolName } from ".";
import { Tool } from "ai";
import { httpFetchTool } from "./http/fetch";
import { jsExecutionTool } from "./code/js-run-tool";
import { pythonExecutionTool } from "./code/python-run-tool";

const { searchTool, contentTool } = getWebSearchTools();

export const APP_DEFAULT_TOOL_KIT: Record<
AppDefaultToolkit,
Record<string, Tool>
Expand All @@ -20,8 +22,8 @@ export const APP_DEFAULT_TOOL_KIT: Record<
[DefaultToolName.CreateTable]: createTableTool,
},
[AppDefaultToolkit.WebSearch]: {
[DefaultToolName.WebSearch]: exaSearchTool,
[DefaultToolName.WebContent]: exaContentsTool,
[DefaultToolName.WebSearch]: searchTool,
[DefaultToolName.WebContent]: contentTool,
},
[AppDefaultToolkit.Http]: {
[DefaultToolName.Http]: httpFetchTool,
Expand Down
191 changes: 190 additions & 1 deletion src/lib/ai/tools/web/web-search.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { tool as createTool } from "ai";
import { tool as createTool, Tool } from "ai";
import { JSONSchema7 } from "json-schema";
import { jsonSchemaToZod } from "lib/json-schema-to-zod";
import { safe } from "ts-safe";
import { tavily } from "@tavily/core";

// Exa API Types
export interface ExaSearchRequest {
Expand Down Expand Up @@ -308,3 +309,191 @@ export const exaContentsTool = createTool({
.unwrap();
},
});

// --- Tavily Search & Extract Tools ---

const getTavilyClient = () => {
const apiKey = process.env.TAVILY_API_KEY;
if (!apiKey) {
throw new Error("TAVILY_API_KEY is not configured");
}
return tavily({ apiKey });
};

export const tavilySearchSchema: JSONSchema7 = {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
maxResults: {
type: "number",
description: "Number of search results to return",
default: 5,
minimum: 1,
maximum: 20,
},
searchDepth: {
type: "string",
enum: ["basic", "advanced"],
description:
"Search depth - basic for quick results, advanced for higher relevance",
default: "basic",
},
topic: {
type: "string",
enum: ["general", "news", "finance"],
description: "Topic category to focus the search on",
default: "general",
},
includeDomains: {
type: "array",
items: { type: "string" },
description: "List of domains to specifically include in search results",
},
excludeDomains: {
type: "array",
items: { type: "string" },
description:
"List of domains to specifically exclude from search results",
},
timeRange: {
type: "string",
enum: ["day", "week", "month", "year"],
description: "Time range filter for results",
},
},
required: ["query"],
};

export const tavilyExtractSchema: JSONSchema7 = {
type: "object",
properties: {
urls: {
type: "array",
items: { type: "string" },
description: "List of URLs to extract content from (max 20)",
},
extractDepth: {
type: "string",
enum: ["basic", "advanced"],
description:
"Extraction depth - basic for fast extraction, advanced for more thorough content",
default: "basic",
},
},
required: ["urls"],
};

export const tavilySearchToolForWorkflow = createTool({
description:
"Search the web using Tavily - performs real-time web searches optimized for LLMs. Returns high-quality, relevant results with content extraction.",
inputSchema: jsonSchemaToZod(tavilySearchSchema),
execute: async (params) => {
const client = getTavilyClient();
return client.search(params.query, {
maxResults: params.maxResults || 5,
searchDepth: params.searchDepth || "basic",
topic: params.topic || "general",
includeDomains: params.includeDomains,
excludeDomains: params.excludeDomains,
timeRange: params.timeRange,
});
},
});

export const tavilyExtractToolForWorkflow = createTool({
description:
"Extract detailed content from specific URLs using Tavily - retrieves full text content and structured information from web pages.",
inputSchema: jsonSchemaToZod(tavilyExtractSchema),
execute: async (params) => {
const client = getTavilyClient();
return client.extract(params.urls, {
extractDepth: params.extractDepth || "basic",
});
},
});

export const tavilySearchTool = createTool({
description:
"Search the web using Tavily - performs real-time web searches optimized for LLMs. Returns high-quality, relevant results with content extraction.",
inputSchema: jsonSchemaToZod(tavilySearchSchema),
execute: (params) => {
return safe(async () => {
const client = getTavilyClient();
const result = await client.search(params.query, {
maxResults: params.maxResults || 5,
searchDepth: params.searchDepth || "basic",
topic: params.topic || "general",
includeDomains: params.includeDomains,
excludeDomains: params.excludeDomains,
timeRange: params.timeRange,
});

return {
...result,
guide: `Use the search results to answer the user's question. Summarize the content and ask if they have any additional questions about the topic.`,
};
})
.ifFail((e) => {
return {
isError: true,
error: e.message,
solution:
"A web search error occurred. First, explain to the user what caused this specific error and how they can resolve it. Then provide helpful information based on your existing knowledge to answer their question.",
};
})
.unwrap();
},
});

export const tavilyExtractTool = createTool({
description:
"Extract detailed content from specific URLs using Tavily - retrieves full text content and structured information from web pages.",
inputSchema: jsonSchemaToZod(tavilyExtractSchema),
execute: async (params) => {
return safe(async () => {
const client = getTavilyClient();
return await client.extract(params.urls, {
extractDepth: params.extractDepth || "basic",
});
})
.ifFail((e) => {
return {
isError: true,
error: e.message,
solution:
"A web content extraction error occurred. First, explain to the user what caused this specific error and how they can resolve it. Then provide helpful information based on your existing knowledge to answer their question.",
};
})
.unwrap();
},
});

// --- Provider factory ---

export function getWebSearchTools(): {
searchTool: Tool;
contentTool: Tool;
searchToolForWorkflow: Tool;
contentToolForWorkflow: Tool;
} {
const provider = process.env.SEARCH_PROVIDER || "exa";

if (provider === "tavily") {
return {
searchTool: tavilySearchTool,
contentTool: tavilyExtractTool,
searchToolForWorkflow: tavilySearchToolForWorkflow,
contentToolForWorkflow: tavilyExtractToolForWorkflow,
};
}

return {
searchTool: exaSearchTool,
contentTool: exaContentsTool,
searchToolForWorkflow: exaSearchToolForWorkflow,
contentToolForWorkflow: exaContentsToolForWorkflow,
};
}
10 changes: 4 additions & 6 deletions src/lib/ai/workflow/executor/node-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ import { jsonSchemaToZod } from "lib/json-schema-to-zod";
import { toAny } from "lib/utils";
import { AppError } from "lib/errors";
import { DefaultToolName } from "lib/ai/tools";
import {
exaSearchToolForWorkflow,
exaContentsToolForWorkflow,
} from "lib/ai/tools/web/web-search";
import { getWebSearchTools } from "lib/ai/tools/web/web-search";
import { mcpClientsManager } from "lib/ai/mcp/mcp-manager";

/**
Expand Down Expand Up @@ -249,11 +246,12 @@ export const toolNodeExecutor: NodeExecutor<ToolNodeData> = async ({
tool_result: toolResult,
};
} else if (node.tool.type == "app-tool") {
const { searchToolForWorkflow, contentToolForWorkflow } = getWebSearchTools();
const executor =
node.tool.id == DefaultToolName.WebContent
? exaContentsToolForWorkflow.execute
? contentToolForWorkflow.execute
: node.tool.id == DefaultToolName.WebSearch
? exaSearchToolForWorkflow.execute
? searchToolForWorkflow.execute
: () => "Unknown tool";

const toolResult = await executor?.(result.input.parameter, {
Expand Down