Initial commit
This commit is contained in:
commit
523576ce96
12 changed files with 784 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
* text=auto eol=lf
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
|
*.env
|
||||||
|
.DS_Store
|
||||||
5
.npmignore
Normal file
5
.npmignore
Normal 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
21
LICENSE.md
Normal 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
164
README.md
Normal 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 Tavily’s `/search` endpoint.
|
||||||
|
2. **Tavily Extract**: Extract structured content from URLs using Tavily’s `/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
43
package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/credentials/TavilyApi.credentials.ts
Normal file
18
src/credentials/TavilyApi.credentials.ts
Normal 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
6
src/index.ts
Normal 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';
|
||||||
130
src/nodes/Tavily/TavilyExtract.node.ts
Normal file
130
src/nodes/Tavily/TavilyExtract.node.ts
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
291
src/nodes/Tavily/TavilySearch.node.ts
Normal file
291
src/nodes/Tavily/TavilySearch.node.ts
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/nodes/Tavily/tavilyApi.utils.ts
Normal file
85
src/nodes/Tavily/tavilyApi.utils.ts
Normal 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
15
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue