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