Initial commit

This commit is contained in:
Steven Milanese 2025-03-07 04:27:10 -05:00
commit 523576ce96
12 changed files with 784 additions and 0 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
dist
*.log
*.env
.DS_Store

5
.npmignore Normal file
View file

@ -0,0 +1,5 @@
# Exclude source files and unneeded config files from the published package
src/
.gitignore
.gitattributes
tsconfig.json

21
LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2024 Steven Milanese
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

164
README.md Normal file
View file

@ -0,0 +1,164 @@
# n8n-tavily-nodes
**n8n-tavily-nodes** is an n8n community node package that integrates the [Tavily API](https://api.tavily.com) for powerful **web search** and **content extraction**.
It contains two nodes:
1. **Tavily Search**: Query the web using Tavilys `/search` endpoint.
2. **Tavily Extract**: Extract structured content from URLs using Tavilys `/extract` endpoint.
---
## Table of Contents
1. [Features](#features)
2. [Installation](#installation)
3. [Getting a Tavily API Key](#getting-a-tavily-api-key)
4. [Configuring Credentials in n8n](#configuring-credentials-in-n8n)
5. [Usage](#usage)
* [Example: Tavily Search Node](#example-tavily-search-node)
* [Example: Tavily Extract Node](#example-tavily-extract-node)
6. [Parameters](#parameters)
* [Tavily Search Parameters](#tavily-search-parameters)
* [Tavily Extract Parameters](#tavily-extract-parameters)
7. [Troubleshooting](#troubleshooting)
8. [License (MIT)](#license-mit)
---
## Features
- **Tavily Search**
* Query the web with multiple filtering options (topic, time range, domain inclusion/exclusion, etc.).
* Optionally retrieve a generated answer, raw content, or images.
- **Tavily Extract**
* Extract text and optional images from one or more URLs.
* Choose between basic or advanced extraction depth.
- **Robust Error Handling**
* Comprehensive input validation with descriptive error messages.
* Catch Tavily API error codes (`400, 401, 403, 429, 500`) and display them in n8n.
- **Easy Setup**
* Install from npm.
* Configure a single **Tavily API** credential in n8n.
---
## Installation
1. From your n8n root directory, run:
```bash
npm install n8n-tavily-nodes
```
2. Restart n8n.
---
## Getting a Tavily API Key
1. Go to the [Tavily website](https://tavily.com) and create an account.
2. Navigate to your dashboard or settings to find your API key.
---
## Configuring Credentials in n8n
1. In your n8n instance, go to the "Credentials" section.
2. Click "Create Credential".
3. Search for or select "Tavily API".
4. Enter your Tavily API key in the appropriate field.
5. Save the credential.
---
## Usage
### Example: Tavily Search Node
1. Add the "Tavily Search" node to your n8n workflow.
2. Connect it to the preceding node in your workflow.
3. In the node's settings:
* Select your Tavily API credential.
* Enter your search query.
* Configure any other desired search parameters (topic, search depth, etc.).
4. Run the workflow to execute the search.
### Example: Tavily Extract Node
1. Add the "Tavily Extract" node to your n8n workflow.
2. Connect it to the preceding node.
3. In the node's settings:
* Select your Tavily API credential.
* Enter the URLs you want to extract content from.
* Configure any other extraction parameters (include images, extract depth).
4. Run the workflow to extract the content.
---
## Parameters
### Tavily Search Parameters
| Parameter | Description |
| :--------------------- | :------------------------------------------------------------------------------------------------------ |
| **Query** | The search query to execute. |
| **Topic** | The category of the search (General or News). |
| **Search Depth** | The depth of the search (Basic or Advanced). |
| **Max Results** | Maximum number of search results to return (0-20). |
| **Time Range** | Time range filter for results (relative to current date). |
| **Days (News Only)** | Number of days back from the current date to include (for News topic). |
| **Include Answer** | Include an LLM-generated answer in the response (No, Basic, or Advanced). |
| **Include Raw Content** | Include cleaned and parsed HTML content of each search result. |
| **Include Images** | Perform an image search and include the results in the response. |
| **Include Image Descriptions** | When including images, also add a descriptive text for each image. |
| **Include Domains** | A list of domains to specifically include in the search results. |
| **Exclude Domains** | A list of domains to specifically exclude from the search results. |
### Tavily Extract Parameters
| Parameter | Description |
| :--------------- | :-------------------------------------------------------------------------- |
| **URLs** | One or more URLs to extract content from. |
| **Include Images** | Include a list of images extracted from each URL. |
| **Extract Depth** | How deeply to parse each URL (Basic or Advanced). |
---
## Troubleshooting
### Common Issues & Error Codes
* **Invalid API Key:** Ensure your Tavily API key is entered correctly in the credentials.
* **Rate Limiting (429):** You have exceeded your Tavily API rate limit. Wait a while before making more requests or upgrade your Tavily plan.
* **Bad Request (400):** Check your input parameters for errors or missing required fields.
* **Other Error Codes (401, 403, 500):** Refer to the Tavily API documentation for details on specific error codes.
---
## License (MIT)
MIT License
Copyright (c) 2025 LEVEL AI XYZ
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

43
package.json Normal file
View file

@ -0,0 +1,43 @@
{
"name": "n8n-tavily-nodes",
"version": "1.0.0",
"description": "n8n Community Nodes for Tavily API integration",
"author": "Steven Milanese <dev@levelai.xyz> (https://StevenMilanese.com)",
"license": "MIT",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"keywords": [
"n8n",
"community-node",
"tavily",
"api",
"search",
"extract"
],
"repository": {
"type": "git",
"url": "https://github.com/developtheweb/n8n-tavily-nodes"
},
"bugs": {
"url": "https://github.com/developtheweb/n8n-tavily-nodes/issues"
},
"dependencies": {
"axios": "^1.6.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
"typescript": "^4.9.0"
},
"n8n": {
"credentials": [
"./dist/credentials/TavilyApi.credentials.js"
],
"nodes": [
"./dist/nodes/Tavily/TavilySearch.node.js",
"./dist/nodes/Tavily/TavilyExtract.node.js"
]
}
}

View file

@ -0,0 +1,18 @@
import {
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class TavilyApi implements ICredentialType {
name = 'tavilyApi';
displayName = 'Tavily API';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
default: '',
description: 'Enter your Tavily API key here (e.g., tvly-abcdef123456)',
},
];
}

6
src/index.ts Normal file
View file

@ -0,0 +1,6 @@
// Exporting credentials
export * from './credentials/TavilyApi.credentials';
// Exporting nodes
export * from './nodes/Tavily/TavilySearch.node';
export * from './nodes/Tavily/TavilyExtract.node';

View file

@ -0,0 +1,130 @@
import { IExecuteFunctions } from 'n8n-core';
import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
INodeProperties,
NodeApiError,
} from 'n8n-workflow';
import { tavilyApiRequest } from './tavilyApi.utils';
export class TavilyExtract implements INodeType {
description: INodeTypeDescription = {
displayName: 'Tavily Extract',
name: 'tavilyExtract',
icon: 'file:tavily.svg',
group: ['transform'],
version: 1,
description: 'Extract web page content using the Tavily Extract endpoint',
defaults: {
name: 'Tavily Extract',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'tavilyApi',
required: true,
},
],
properties: [
{
displayName: 'URLs',
name: 'urls',
type: 'string',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add URL',
},
default: [],
required: true,
description: 'One or more URLs to extract content from',
},
{
displayName: 'Include Images',
name: 'includeImages',
type: 'boolean',
default: false,
description: 'Include a list of images extracted from each URL',
},
{
displayName: 'Extract Depth',
name: 'extractDepth',
type: 'options',
options: [
{ name: 'Basic', value: 'basic' },
{ name: 'Advanced', value: 'advanced' },
],
default: 'basic',
description: 'How deeply to parse each URL. "advanced" retrieves more data but can increase latency',
},
{
displayName: 'API Documentation',
name: 'apiDocumentationNotice',
type: 'notice',
default: '',
description: `### Tavily Extract API
**Endpoint**: POST /extract
**Body Parameters**:
- urls (string[]; required)
- include_images (boolean)
- extract_depth (basic|advanced)
**Response**:
- results
- failed_results
- response_time
`,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('tavilyApi');
if (!credentials?.apiKey) {
throw new NodeApiError(this.getNode(), new Error('Missing Tavily API key in credentials.'));
}
const apiKey = credentials.apiKey as string;
for (let i = 0; i < items.length; i++) {
try {
const urls = this.getNodeParameter('urls', i, []) as string[];
const includeImages = this.getNodeParameter('includeImages', i) as boolean;
const extractDepth = this.getNodeParameter('extractDepth', i) as string;
// Validate input parameters
if (!Array.isArray(urls) || urls.length === 0) {
throw new NodeApiError(this.getNode(), new Error('At least one URL is required.'));
}
const body = {
urls,
include_images: includeImages,
extract_depth: extractDepth,
};
const result = await tavilyApiRequest.call(
this,
'POST',
'/extract',
body,
apiKey,
);
returnData.push({ json: result });
} catch (error) {
if (error instanceof NodeApiError) {
throw error;
}
throw new NodeApiError(this.getNode(), error as Error);
}
}
return [returnData];
}
}

View file

@ -0,0 +1,291 @@
import { IExecuteFunctions } from 'n8n-core';
import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
INodeProperties,
NodeApiError,
} from 'n8n-workflow';
import { tavilyApiRequest } from './tavilyApi.utils';
// NEW: maintain a constant array for valid time ranges
const ALLOWED_TIME_RANGES = ['', 'day', 'week', 'month', 'year', 'd', 'w', 'm', 'y'];
export class TavilySearch implements INodeType {
description: INodeTypeDescription = {
displayName: 'Tavily Search',
name: 'tavilySearch',
icon: 'file:tavily.svg',
group: ['transform'],
version: 1,
description: 'Execute a search query using the Tavily Search endpoint',
defaults: {
name: 'Tavily Search',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'tavilyApi',
required: true,
},
],
properties: [
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
required: true,
description: 'The search query to execute with Tavily',
},
{
displayName: 'Topic',
name: 'topic',
type: 'options',
options: [
{ name: 'General', value: 'general' },
{ name: 'News', value: 'news' },
],
default: 'general',
description: 'The category of the search',
},
{
displayName: 'Search Depth',
name: 'searchDepth',
type: 'options',
options: [
{ name: 'Basic', value: 'basic' },
{ name: 'Advanced', value: 'advanced' },
],
default: 'basic',
description: 'Depth of the search (basic=1 credit, advanced=2 credits)',
},
{
displayName: 'Max Results',
name: 'maxResults',
type: 'number',
typeOptions: {
minValue: 0,
maxValue: 20,
},
default: 5,
description: 'Maximum number of search results to return (0-20)',
},
{
displayName: 'Time Range',
name: 'timeRange',
type: 'options',
options: [
{ name: 'None', value: '' },
{ name: 'Day', value: 'day' },
{ name: 'Week', value: 'week' },
{ name: 'Month', value: 'month' },
{ name: 'Year', value: 'year' },
{ name: 'd', value: 'd' },
{ name: 'w', value: 'w' },
{ name: 'm', value: 'm' },
{ name: 'y', value: 'y' },
],
default: '',
description: 'Time range filter (relative to current date)',
},
{
displayName: 'Days (News Only)',
name: 'days',
type: 'number',
typeOptions: {
minValue: 0,
},
default: 3,
description: 'Number of days back from the current date to include (for topic=news)',
displayOptions: {
show: {
topic: ['news'],
},
},
},
{
displayName: 'Include Answer',
name: 'includeAnswer',
type: 'options',
options: [
{ name: 'No', value: 'false' },
{ name: 'Basic', value: 'basic' },
{ name: 'Advanced', value: 'advanced' },
],
default: 'false',
description: 'Include an LLM-generated answer (basic or advanced)',
},
{
displayName: 'Include Raw Content',
name: 'includeRawContent',
type: 'boolean',
default: false,
description: 'Include cleaned and parsed HTML content of each search result',
},
{
displayName: 'Include Images',
name: 'includeImages',
type: 'boolean',
default: false,
description: 'Perform an image search and include images in the response',
},
{
displayName: 'Include Image Descriptions',
name: 'includeImageDescriptions',
type: 'boolean',
displayOptions: {
show: {
includeImages: [true],
},
},
default: false,
description: 'When including images, also add a descriptive text for each image',
},
{
displayName: 'Include Domains',
name: 'includeDomains',
type: 'string',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Domain',
},
default: [],
description: 'Domains to specifically include in the search results',
},
{
displayName: 'Exclude Domains',
name: 'excludeDomains',
type: 'string',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Domain',
},
default: [],
description: 'Domains to specifically exclude from the search results',
},
{
displayName: 'API Documentation',
name: 'apiDocumentationNotice',
type: 'notice',
default: '',
description: `### Tavily Search API
**Endpoint**: POST /search
**Body Parameters**:
- query (required)
- topic (general|news)
- search_depth (basic|advanced)
- max_results (0-20)
- time_range (day|week|month|year|d|w|m|y)
- days (>=0, news only)
- include_answer (false|basic|advanced)
- include_raw_content (boolean)
- include_images (boolean)
- include_image_descriptions (boolean)
- include_domains (string[])
- exclude_domains (string[])
**Response**:
- query
- answer (optional)
- images (optional)
- results
- response_time
`,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('tavilyApi');
if (!credentials?.apiKey) {
// Standardize NodeApiError usage
throw new NodeApiError(this.getNode(), new Error('Missing Tavily API key in credentials.'));
}
const apiKey = credentials.apiKey as string;
for (let i = 0; i < items.length; i++) {
try {
const query = this.getNodeParameter('query', i) as string;
const topic = this.getNodeParameter('topic', i) as string;
const searchDepth = this.getNodeParameter('searchDepth', i) as string;
const maxResults = this.getNodeParameter('maxResults', i) as number;
const timeRange = this.getNodeParameter('timeRange', i) as string;
const days = this.getNodeParameter('days', i) as number;
const includeAnswer = this.getNodeParameter('includeAnswer', i) as string;
const includeRawContent = this.getNodeParameter('includeRawContent', i) as boolean;
const includeImages = this.getNodeParameter('includeImages', i) as boolean;
const includeImageDescriptions = this.getNodeParameter('includeImageDescriptions', i) as boolean;
const includeDomains = this.getNodeParameter('includeDomains', i, []) as string[];
const excludeDomains = this.getNodeParameter('excludeDomains', i, []) as string[];
// --- Validate input parameters ---
if (!query?.trim()) {
throw new NodeApiError(this.getNode(), new Error('Query parameter cannot be empty.'));
}
if (maxResults < 0 || maxResults > 20) {
throw new NodeApiError(this.getNode(), new Error('Max Results must be between 0 and 20.'));
}
// Use ALLOWED_TIME_RANGES constant
if (!ALLOWED_TIME_RANGES.includes(timeRange)) {
throw new NodeApiError(this.getNode(), new Error(`Invalid timeRange value. Allowed: ${ALLOWED_TIME_RANGES.join(', ')}`));
}
if (topic === 'news' && days < 0) {
throw new NodeApiError(this.getNode(), new Error('Days must be >= 0 when topic is "news".'));
}
// ----------------------------------
const body: Record<string, any> = {
query,
topic,
search_depth: searchDepth,
max_results: maxResults,
include_raw_content: includeRawContent,
include_images: includeImages,
include_image_descriptions: includeImageDescriptions,
};
if (timeRange) {
body.time_range = timeRange;
}
if (topic === 'news') {
body.days = days;
}
if (includeAnswer !== 'false') {
body.include_answer = includeAnswer;
}
if (Array.isArray(includeDomains) && includeDomains.length > 0) {
body.include_domains = includeDomains;
}
if (Array.isArray(excludeDomains) && excludeDomains.length > 0) {
body.exclude_domains = excludeDomains;
}
// Make request via the shared utility
const result = await tavilyApiRequest.call(
this,
'POST',
'/search',
body,
apiKey,
);
returnData.push({ json: result });
} catch (error) {
// Ensure consistent error wrapping
if (error instanceof NodeApiError) {
throw error;
}
throw new NodeApiError(this.getNode(), error as Error);
}
}
return [returnData];
}
}

View file

@ -0,0 +1,85 @@
import axios, { AxiosError, AxiosResponse } from 'axios';
import { NodeApiError } from 'n8n-workflow';
import { IExecuteFunctions } from 'n8n-core';
/**
* Common function to call the Tavily API.
* Handles request execution and standard error handling.
*/
export async function tavilyApiRequest(
this: IExecuteFunctions,
method: 'POST',
resource: '/search' | '/extract',
body: Record<string, any>,
apiKey: string,
): Promise<any> {
let response: AxiosResponse<any>;
try {
response = await axios.request({
method,
url: `https://api.tavily.com${resource}`,
data: body,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
});
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
let message = `Tavily API error: ${error.message}`;
if (status) {
switch (status) {
case 400:
message = 'Tavily error [400]: Bad Request.';
break;
case 401:
message = 'Tavily error [401]: Unauthorized.';
break;
case 403:
message = 'Tavily error [403]: Forbidden.';
break;
case 429:
message = 'Tavily error [429]: Too many requests.';
break;
case 500:
message = 'Tavily error [500]: Internal Server Error.';
break;
default:
message = `Tavily API error [${status}]: ${error.message}`;
break;
}
}
// If the Tavily API returned a body with error details, append them
if (error.response?.data) {
const data = error.response.data;
const additionalMsgParts: string[] = [];
if (data.error_code) {
additionalMsgParts.push(`error_code: ${data.error_code}`);
}
if (data.error_message) {
additionalMsgParts.push(`error_message: ${data.error_message}`);
}
if (!data.error_code && !data.error_message) {
// Fall back to a full JSON dump if no known fields
additionalMsgParts.push(JSON.stringify(data));
}
// Append to main message
if (additionalMsgParts.length > 0) {
message += ` | ${additionalMsgParts.join(' | ')}`;
}
}
throw new NodeApiError(this.getNode(), new Error(message));
} else {
throw new NodeApiError(this.getNode(), error as Error);
}
}
return response.data;
}

15
tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "CommonJS",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}