mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat(Perplexity Node): Update Perplexity node for full API coverage (#26970)
Co-authored-by: Dimitri Lavrenük <20122620+dlavrenuek@users.noreply.github.com>
This commit is contained in:
co-authored by
Dimitri Lavrenük
parent
2ff73d5440
commit
483250d539
@@ -29,6 +29,8 @@ export class PerplexityApi implements ICredentialType {
|
||||
properties: {
|
||||
headers: {
|
||||
Authorization: '=Bearer {{$credentials.apiKey}}',
|
||||
'User-Agent': 'n8n',
|
||||
'X-Source': 'n8n',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type {
|
||||
IExecuteSingleFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IN8nHttpFullResponse,
|
||||
INodeExecutionData,
|
||||
INodeListSearchResult,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
@@ -39,3 +41,57 @@ export async function sendErrorPostReceive(
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const createErrorHandler = (description: string) =>
|
||||
async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
data: INodeExecutionData[],
|
||||
{ statusCode, body, statusMessage }: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
if (statusCode >= 400 && statusCode <= 599) {
|
||||
const errorBody = body as JsonObject;
|
||||
const error = (errorBody?.error ?? errorBody) as JsonObject;
|
||||
const errorMessage =
|
||||
typeof error.message === 'string'
|
||||
? error.message
|
||||
: (statusMessage ?? 'An unexpected issue occurred');
|
||||
|
||||
throw new NodeApiError(this.getNode(), errorBody, { message: errorMessage, description });
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const agentErrorPostReceive = createErrorHandler(
|
||||
'Refer to the Agent API documentation at https://docs.perplexity.ai/api-reference/agent-post for valid parameters.',
|
||||
);
|
||||
|
||||
export const searchErrorPostReceive = createErrorHandler(
|
||||
'Refer to the Search API documentation at https://docs.perplexity.ai/api-reference/search-post for valid parameters.',
|
||||
);
|
||||
|
||||
export const embeddingsErrorPostReceive = createErrorHandler(
|
||||
'Refer to the Embeddings API documentation at https://docs.perplexity.ai/api-reference/embeddings-post for valid parameters.',
|
||||
);
|
||||
|
||||
export async function getAgentModels(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter: string = '',
|
||||
): Promise<INodeListSearchResult> {
|
||||
const response = (await this.helpers.requestWithAuthentication.call(this, 'perplexityApi', {
|
||||
method: 'GET',
|
||||
url: 'https://api.perplexity.ai/v1/models',
|
||||
json: true,
|
||||
})) as { data?: Array<{ id: string; owned_by: string }> };
|
||||
|
||||
const models = response.data ?? [];
|
||||
|
||||
const results = models
|
||||
.map((model) => ({
|
||||
name: model.id,
|
||||
value: model.id,
|
||||
url: 'https://docs.perplexity.ai/docs/agent-api/models',
|
||||
}))
|
||||
.filter((item) => !filter || item.name.toLowerCase().includes(filter.toLowerCase()));
|
||||
|
||||
return { results };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.perplexity",
|
||||
"nodeVersion": "1.0",
|
||||
"nodeVersion": "1.1",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Utility"],
|
||||
"categories": ["AI", "Utility"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { chat } from './descriptions';
|
||||
import { agent, chat, embeddings, search } from './descriptions';
|
||||
import { getAgentModels } from './GenericFunctions';
|
||||
|
||||
export class Perplexity implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -12,10 +13,11 @@ export class Perplexity implements INodeType {
|
||||
dark: 'file:perplexity.dark.svg',
|
||||
},
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
version: [1, 2],
|
||||
defaultVersion: 2,
|
||||
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
|
||||
description:
|
||||
' AI-powered answer engine that provides accurate, trusted, and real-time answers to any question. Generates AI responses with citations',
|
||||
'AI-powered answer engine that provides accurate, trusted, and real-time answers to any question. Supports chat completions, agent responses, web search, and embeddings.',
|
||||
defaults: {
|
||||
name: 'Perplexity',
|
||||
},
|
||||
@@ -33,6 +35,7 @@ export class Perplexity implements INodeType {
|
||||
ignoreHttpStatusErrors: true,
|
||||
},
|
||||
properties: [
|
||||
// V1: hidden resource selector (only chat)
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
@@ -45,8 +48,58 @@ export class Perplexity implements INodeType {
|
||||
},
|
||||
],
|
||||
default: 'chat',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
// V2: visible resource selector (all resources)
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Agent',
|
||||
value: 'agent',
|
||||
description:
|
||||
'Create responses using the Agent API with third-party models, presets, tools, and structured outputs',
|
||||
},
|
||||
{
|
||||
name: 'Chat',
|
||||
value: 'chat',
|
||||
description: 'Send messages using Sonar models with built-in web search',
|
||||
},
|
||||
{
|
||||
name: 'Embedding',
|
||||
value: 'embedding',
|
||||
description: 'Generate vector embeddings for text',
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
description: 'Get raw, ranked web search results',
|
||||
},
|
||||
],
|
||||
default: 'chat',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
},
|
||||
...agent.description,
|
||||
...chat.description,
|
||||
...embeddings.description,
|
||||
...search.description,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
listSearch: {
|
||||
getAgentModels,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { agentErrorPostReceive } from '../../GenericFunctions';
|
||||
import * as createResponse from './createResponse.operation';
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['agent'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create Response',
|
||||
value: 'createResponse',
|
||||
action: 'Create a response',
|
||||
description:
|
||||
'Create a response using the Agent API with third-party models, presets, and tools',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/v1/agent',
|
||||
},
|
||||
output: {
|
||||
postReceive: [agentErrorPostReceive],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
default: 'createResponse',
|
||||
},
|
||||
|
||||
...createResponse.description,
|
||||
];
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Input',
|
||||
name: 'input',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
typeOptions: { rows: 3 },
|
||||
description: 'The input text prompt to send to the agent',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'input',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Model',
|
||||
name: 'model',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
description:
|
||||
'The model to use. Uses provider/model format (e.g. openai/gpt-5.2). Leave empty when using a preset.',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'getAgentModels',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. openai/gpt-5.2',
|
||||
},
|
||||
],
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'model',
|
||||
value: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Preset',
|
||||
name: 'preset',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Preset name to use. Use preset OR model, not both.',
|
||||
hint: 'Use a preset OR a model, not both',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'preset',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return only essential fields (ID, model, output text, citations)',
|
||||
routing: {
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
enabled: '={{ $value }}',
|
||||
properties: {
|
||||
value:
|
||||
'={{ { "id": $response.body?.id, "model": $response.body?.model, "output_text": $response.body?.output?.find(o => o.type === "message")?.content?.find(c => c.type === "output_text")?.text, "citations": $response.body?.output?.find(o => o.type === "search_results")?.results?.map(r => ({ title: r.title, url: r.url })), "usage": $response.body?.usage } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Instructions',
|
||||
name: 'instructions',
|
||||
type: 'string',
|
||||
default: '',
|
||||
typeOptions: { rows: 3 },
|
||||
description: 'System-level instructions for the agent',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'instructions',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Language Preference',
|
||||
name: 'languagePreference',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. en',
|
||||
description: 'ISO 639-1 language code for the response language preference',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'language_preference',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Max Output Tokens',
|
||||
name: 'maxOutputTokens',
|
||||
type: 'number',
|
||||
default: 1024,
|
||||
typeOptions: { minValue: 1 },
|
||||
description: 'The maximum number of tokens to generate in the response',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'max_output_tokens',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Max Steps',
|
||||
name: 'maxSteps',
|
||||
type: 'number',
|
||||
default: 5,
|
||||
typeOptions: { minValue: 1, maxValue: 10 },
|
||||
description: 'Maximum number of agentic steps (1-10)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'max_steps',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Models (Fallback)',
|
||||
name: 'modelsFallback',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. openai/gpt-5.2,anthropic/claude-sonnet-4-6',
|
||||
description: 'Comma-separated list of 1-5 model IDs to use as fallbacks',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'models',
|
||||
value: '={{ $value.split(",").map(s => s.trim()).filter(s => s) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Reasoning',
|
||||
name: 'reasoning',
|
||||
type: 'json',
|
||||
default: '',
|
||||
description: 'Reasoning configuration object (e.g. {"effort": "high"})',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'reasoning',
|
||||
value: '={{ JSON.parse($value) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Response Format',
|
||||
name: 'responseFormat',
|
||||
type: 'json',
|
||||
default: '',
|
||||
description:
|
||||
'JSON schema for structured output. Set type to "json_schema" with a schema property.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'response_format',
|
||||
value: '={{ JSON.parse($value) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tools',
|
||||
name: 'tools',
|
||||
type: 'json',
|
||||
default: '',
|
||||
placeholder: 'e.g. [{"type":"web_search"},{"type":"fetch_url"}]',
|
||||
description: 'Array of tool objects to make available to the agent',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'tools',
|
||||
value: '={{ JSON.parse($value) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['agent'],
|
||||
operation: ['createResponse'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
@@ -12,7 +12,6 @@ const properties: INodeProperties[] = [
|
||||
{ name: 'Sonar', value: 'sonar' },
|
||||
{ name: 'Sonar Deep Research', value: 'sonar-deep-research' },
|
||||
{ name: 'Sonar Pro', value: 'sonar-pro' },
|
||||
{ name: 'Sonar Reasoning', value: 'sonar-reasoning' },
|
||||
{ name: 'Sonar Reasoning Pro', value: 'sonar-reasoning-pro' },
|
||||
],
|
||||
description: 'The model which will generate the completion',
|
||||
@@ -125,6 +124,42 @@ const properties: INodeProperties[] = [
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Disable Search',
|
||||
name: 'disableSearch',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to disable web search for this request',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'disable_search',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Enable Search Classifier',
|
||||
name: 'enableSearchClassifier',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to enable the search classifier',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'enable_search_classifier',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Frequency Penalty',
|
||||
name: 'frequencyPenalty',
|
||||
@@ -142,6 +177,103 @@ const properties: INodeProperties[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Image Domain Filter',
|
||||
name: 'imageDomainFilter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. domain1.com,domain2.com',
|
||||
description: 'Comma-separated list of domains to filter image results from',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'image_domain_filter',
|
||||
value: '={{ $value.split(",").map(s => s.trim()).filter(s => s) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Image Format Filter',
|
||||
name: 'imageFormatFilter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. jpg,png',
|
||||
description: 'Comma-separated list of image formats to filter results by',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'image_format_filter',
|
||||
value: '={{ $value.split(",").map(s => s.trim()).filter(s => s) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Language Preference',
|
||||
name: 'languagePreference',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. en',
|
||||
description: 'ISO 639-1 language code for the response language preference',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'language_preference',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Updated After',
|
||||
name: 'lastUpdatedAfter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. 01/01/2024',
|
||||
description: 'Filter results last updated after this date (MM/DD/YYYY)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'last_updated_after_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Updated Before',
|
||||
name: 'lastUpdatedBefore',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. 12/31/2024',
|
||||
description: 'Filter results last updated before this date (MM/DD/YYYY)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'last_updated_before_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Maximum Number of Tokens',
|
||||
name: 'maxTokens',
|
||||
@@ -174,6 +306,234 @@ const properties: INodeProperties[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Presence Penalty',
|
||||
name: 'presencePenalty',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description:
|
||||
"A value between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.",
|
||||
typeOptions: {
|
||||
minValue: -2.0,
|
||||
maxValue: 2.0,
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'presence_penalty',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Reasoning Effort',
|
||||
name: 'reasoningEffort',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Minimal', value: 'minimal' },
|
||||
{ name: 'Low', value: 'low' },
|
||||
{ name: 'Medium', value: 'medium' },
|
||||
{ name: 'High', value: 'high' },
|
||||
],
|
||||
default: 'medium',
|
||||
description: 'The level of reasoning effort to apply',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'reasoning_effort',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Response Format',
|
||||
name: 'responseFormat',
|
||||
type: 'json',
|
||||
default: '',
|
||||
description:
|
||||
'JSON schema for structured output. Set type to "json_schema" with a schema property.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'response_format',
|
||||
value: '={{ JSON.parse($value) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Return Images',
|
||||
name: 'returnImages',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether or not a request to an online model should return images. Requires Perplexity API usage Tier-2.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'return_images',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Return Related Questions',
|
||||
name: 'returnRelatedQuestions',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether or not a request to an online model should return related questions. Requires Perplexity API usage Tier-2.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'return_related_questions',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search After Date',
|
||||
name: 'searchAfterDate',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. 01/01/2024',
|
||||
description: 'Filter results published after this date (MM/DD/YYYY)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_after_date_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Before Date',
|
||||
name: 'searchBeforeDate',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. 12/31/2024',
|
||||
description: 'Filter results published before this date (MM/DD/YYYY)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_before_date_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Domain Filter',
|
||||
name: 'searchDomainFilter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Limit the citations used by the online model to URLs from the specified domains. For blacklisting, add a <code>-</code> to the beginning of the domain string (e.g., <code>-domain1</code>). Currently limited to 3 domains. Requires Perplexity API usage Tier-3.',
|
||||
placeholder: 'e.g. domain1,domain2,-domain3',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_domain_filter',
|
||||
value: '={{ $value.split(",").map(domain => domain.trim()) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Language Filter',
|
||||
name: 'searchLanguageFilter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. en,fr,de',
|
||||
description:
|
||||
'Comma-separated list of ISO 639-1 language codes to filter results by (max 20)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_language_filter',
|
||||
value: '={{ $value.split(",").map(s => s.trim()).filter(s => s) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Mode',
|
||||
name: 'searchMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Web', value: 'web' },
|
||||
{ name: 'Academic', value: 'academic' },
|
||||
{ name: 'SEC', value: 'sec' },
|
||||
],
|
||||
default: 'web',
|
||||
description: 'The search mode to use for retrieving information',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_mode',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Recency Filter',
|
||||
name: 'searchRecency',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Day', value: 'day' },
|
||||
{ name: 'Hour', value: 'hour' },
|
||||
{ name: 'Month', value: 'month' },
|
||||
{ name: 'Week', value: 'week' },
|
||||
{ name: 'Year', value: 'year' },
|
||||
],
|
||||
default: 'month',
|
||||
description: 'Returns search results within the specified time interval',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_recency_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Stop Sequences',
|
||||
name: 'stop',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. stop1,stop2',
|
||||
description: 'Comma-separated list of sequences where the model should stop generating',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'stop',
|
||||
value: '={{ $value.split(",").map(s => s.trim()).filter(s => s) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Top K',
|
||||
name: 'topK',
|
||||
@@ -211,95 +571,21 @@ const properties: INodeProperties[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Presence Penalty',
|
||||
name: 'presencePenalty',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description:
|
||||
"A value between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.",
|
||||
typeOptions: {
|
||||
minValue: -2.0,
|
||||
maxValue: 2.0,
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'presence_penalty',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Return Images',
|
||||
name: 'returnImages',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether or not a request to an online model should return images. Requires Perplexity API usage Tier-2.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'return_images',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Return Related Questions',
|
||||
name: 'returnRelatedQuestions',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether or not a request to an online model should return related questions. Requires Perplexity API usage Tier-2.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'return_related_questions',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Domain Filter',
|
||||
name: 'searchDomainFilter',
|
||||
type: 'string',
|
||||
displayName: 'Web Search Options',
|
||||
name: 'webSearchOptions',
|
||||
type: 'json',
|
||||
default: '',
|
||||
description:
|
||||
'Limit the citations used by the online model to URLs from the specified domains. For blacklisting, add a <code>-</code> to the beginning of the domain string (e.g., <code>-domain1</code>). Currently limited to 3 domains. Requires Perplexity API usage Tier-3.',
|
||||
placeholder: 'e.g. domain1,domain2,-domain3',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_domain_filter',
|
||||
value: '={{ $value.split(",").map(domain => domain.trim()) }}',
|
||||
description: 'Advanced web search configuration object',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Recency Filter',
|
||||
name: 'searchRecency',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Day',
|
||||
value: 'day',
|
||||
},
|
||||
{
|
||||
name: 'Hour',
|
||||
value: 'hour',
|
||||
},
|
||||
{
|
||||
name: 'Month',
|
||||
value: 'month',
|
||||
},
|
||||
{
|
||||
name: 'Week',
|
||||
value: 'week',
|
||||
},
|
||||
],
|
||||
default: 'month',
|
||||
description: 'Returns search results within the specified time interval',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_recency',
|
||||
property: 'web_search_options',
|
||||
value: '={{ JSON.parse($value) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { embeddingsErrorPostReceive } from '../../GenericFunctions';
|
||||
import * as createContextualized from './createContextualized.operation';
|
||||
import * as createEmbedding from './createEmbedding.operation';
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['embedding'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create Embedding',
|
||||
value: 'createEmbedding',
|
||||
action: 'Create an embedding',
|
||||
description: 'Generate vector embeddings for text input',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/v1/embeddings',
|
||||
},
|
||||
output: {
|
||||
postReceive: [embeddingsErrorPostReceive],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Create Contextualized Embedding',
|
||||
value: 'createContextualized',
|
||||
action: 'Create a contextualized embedding',
|
||||
description: 'Generate context-aware embeddings for document chunks',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/v1/contextualizedembeddings',
|
||||
},
|
||||
output: {
|
||||
postReceive: [embeddingsErrorPostReceive],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
default: 'createEmbedding',
|
||||
},
|
||||
|
||||
...createEmbedding.description,
|
||||
...createContextualized.description,
|
||||
];
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Model',
|
||||
name: 'model',
|
||||
type: 'options',
|
||||
required: true,
|
||||
options: [{ name: 'PPLX Embed Context V1 4B', value: 'pplx-embed-context-v1-4b' }],
|
||||
default: 'pplx-embed-context-v1-4b',
|
||||
description: 'The contextualized embedding model to use',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'model',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Documents',
|
||||
name: 'input',
|
||||
type: 'json',
|
||||
required: true,
|
||||
default: '[["paragraph 1 of doc A", "paragraph 2 of doc A"], ["paragraph 1 of doc B"]]',
|
||||
description: 'Array of documents, where each document is an array of paragraph strings',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'input',
|
||||
value: '={{ JSON.parse($value) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Dimensions',
|
||||
name: 'dimensions',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
numberPrecision: 0,
|
||||
},
|
||||
description:
|
||||
'Number of dimensions for the output embedding. If 0 or unset, the full model dimensions are used.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dimensions',
|
||||
value: '={{ $value || undefined }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Encoding Format',
|
||||
name: 'encoding_format',
|
||||
type: 'options',
|
||||
default: 'base64_int8',
|
||||
options: [
|
||||
{ name: 'Base64 Int8', value: 'base64_int8' },
|
||||
{ name: 'Base64 Binary', value: 'base64_binary' },
|
||||
],
|
||||
description:
|
||||
'The format of the returned embeddings. Float is not supported by this endpoint.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'encoding_format',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['embedding'],
|
||||
operation: ['createContextualized'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Model',
|
||||
name: 'model',
|
||||
type: 'options',
|
||||
required: true,
|
||||
options: [
|
||||
{ name: 'PPLX Embed V1 0.6B', value: 'pplx-embed-v1-0.6b' },
|
||||
{ name: 'PPLX Embed V1 4B', value: 'pplx-embed-v1-4b' },
|
||||
],
|
||||
default: 'pplx-embed-v1-4b',
|
||||
description: 'The embedding model to use',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'model',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Texts',
|
||||
name: 'input',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
typeOptions: { rows: 4 },
|
||||
placeholder: 'One text per line',
|
||||
description: 'Text(s) to embed. Put each text on a separate line.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'input',
|
||||
value: '={{ $value.split("\\n").map(s => s.trim()).filter(s => s) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Dimensions',
|
||||
name: 'dimensions',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
numberPrecision: 0,
|
||||
},
|
||||
description:
|
||||
'Number of dimensions for the output embedding. If 0 or unset, the full model dimensions are used.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dimensions',
|
||||
value: '={{ $value || undefined }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Encoding Format',
|
||||
name: 'encoding_format',
|
||||
type: 'options',
|
||||
default: 'base64_int8',
|
||||
options: [
|
||||
{ name: 'Base64 Int8', value: 'base64_int8' },
|
||||
{ name: 'Base64 Binary', value: 'base64_binary' },
|
||||
],
|
||||
description: 'The format of the returned embeddings',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'encoding_format',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['embedding'],
|
||||
operation: ['createEmbedding'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
@@ -1 +1,4 @@
|
||||
export * as agent from './agent/Agent.resource';
|
||||
export * as chat from './chat/Chat.resource';
|
||||
export * as embeddings from './embeddings/Embeddings.resource';
|
||||
export * as search from './search/Search.resource';
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { searchErrorPostReceive } from '../../GenericFunctions';
|
||||
import * as searchOp from './search.operation';
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['search'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
action: 'Search the web',
|
||||
description: 'Get raw, ranked web search results with filtering and content extraction',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/search',
|
||||
},
|
||||
output: {
|
||||
postReceive: [searchErrorPostReceive],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
default: 'search',
|
||||
},
|
||||
|
||||
...searchOp.description,
|
||||
];
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The search query string',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'query',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return only the ID and results array',
|
||||
routing: {
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
enabled: '={{ $value }}',
|
||||
properties: {
|
||||
value: '={{ { "id": $response.body?.id, "results": $response.body?.results } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. us',
|
||||
description: '2-character ISO 3166-1 alpha-2 country code to localize search results',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'country',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Updated After',
|
||||
name: 'lastUpdatedAfter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. 01/01/2024',
|
||||
description: 'Filter results last updated after this date (MM/DD/YYYY)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'last_updated_after_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Updated Before',
|
||||
name: 'lastUpdatedBefore',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. 12/31/2024',
|
||||
description: 'Filter results last updated before this date (MM/DD/YYYY)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'last_updated_before_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Max Results',
|
||||
name: 'maxResults',
|
||||
type: 'number',
|
||||
default: 10,
|
||||
typeOptions: { minValue: 1, maxValue: 20 },
|
||||
description: 'Maximum number of search results to return (1-20)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'max_results',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Max Tokens',
|
||||
name: 'maxTokens',
|
||||
type: 'number',
|
||||
default: 10000,
|
||||
typeOptions: { minValue: 1, maxValue: 1000000 },
|
||||
description: 'Maximum number of tokens in the response',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'max_tokens',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Max Tokens Per Page',
|
||||
name: 'maxTokensPerPage',
|
||||
type: 'number',
|
||||
default: 4096,
|
||||
typeOptions: { minValue: 1, maxValue: 1000000 },
|
||||
description: 'Maximum number of tokens per page of results',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'max_tokens_per_page',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search After Date',
|
||||
name: 'searchAfterDate',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. 01/01/2024',
|
||||
description: 'Filter results published after this date (MM/DD/YYYY)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_after_date_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Before Date',
|
||||
name: 'searchBeforeDate',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. 12/31/2024',
|
||||
description: 'Filter results published before this date (MM/DD/YYYY)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_before_date_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Domain Filter',
|
||||
name: 'searchDomainFilter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. domain1.com,domain2.com',
|
||||
description: 'Comma-separated list of domains to limit search results to (max 20)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_domain_filter',
|
||||
value: '={{ $value.split(",").map(s => s.trim()).filter(s => s) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Language Filter',
|
||||
name: 'searchLanguageFilter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. en,fr,de',
|
||||
description:
|
||||
'Comma-separated list of ISO 639-1 language codes to filter results by (max 20)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_language_filter',
|
||||
value: '={{ $value.split(",").map(s => s.trim()).filter(s => s) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Search Recency Filter',
|
||||
name: 'searchRecencyFilter',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Day', value: 'day' },
|
||||
{ name: 'Hour', value: 'hour' },
|
||||
{ name: 'Month', value: 'month' },
|
||||
{ name: 'Week', value: 'week' },
|
||||
{ name: 'Year', value: 'year' },
|
||||
],
|
||||
default: 'month',
|
||||
description: 'Filter search results by publication recency',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'search_recency_filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['search'],
|
||||
operation: ['search'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
@@ -5,7 +5,12 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { sendErrorPostReceive } from '../GenericFunctions';
|
||||
import {
|
||||
agentErrorPostReceive,
|
||||
embeddingsErrorPostReceive,
|
||||
searchErrorPostReceive,
|
||||
sendErrorPostReceive,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
// Mock implementation for `this` in `sendErrorPostReceive`
|
||||
const mockExecuteSingleFunctions = {
|
||||
@@ -202,4 +207,167 @@ describe('Generic Functions', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('agentErrorPostReceive', () => {
|
||||
let testData: INodeExecutionData[];
|
||||
let testResponse: IN8nHttpFullResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
testData = [{ json: {} }];
|
||||
testResponse = { statusCode: 200, headers: {}, body: {} };
|
||||
});
|
||||
|
||||
it('should return data if status code is not 4xx or 5xx', async () => {
|
||||
const result = await agentErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
testResponse,
|
||||
);
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError if status code is 4xx', async () => {
|
||||
testResponse.statusCode = 400;
|
||||
testResponse.body = { error: { message: 'Bad request' } };
|
||||
await expect(
|
||||
agentErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError if status code is 5xx', async () => {
|
||||
testResponse.statusCode = 500;
|
||||
testResponse.body = { error: { message: 'Server error' } };
|
||||
await expect(
|
||||
agentErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should use statusMessage as fallback when error message is not a string', async () => {
|
||||
const errorResponse = {
|
||||
statusCode: 400,
|
||||
statusMessage: 'Bad Request',
|
||||
headers: {},
|
||||
body: { error: { message: { detail: 'complex error' } } },
|
||||
};
|
||||
await expect(
|
||||
agentErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
errorResponse as unknown as IN8nHttpFullResponse,
|
||||
),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should include agent API documentation in the description', async () => {
|
||||
testResponse.statusCode = 400;
|
||||
testResponse.body = { error: { message: 'Invalid param' } };
|
||||
await expect(
|
||||
agentErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
description: expect.stringContaining(
|
||||
'https://docs.perplexity.ai/api-reference/agent-post',
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchErrorPostReceive', () => {
|
||||
let testData: INodeExecutionData[];
|
||||
let testResponse: IN8nHttpFullResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
testData = [{ json: {} }];
|
||||
testResponse = { statusCode: 200, headers: {}, body: {} };
|
||||
});
|
||||
|
||||
it('should return data if status code is not 4xx or 5xx', async () => {
|
||||
const result = await searchErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
testResponse,
|
||||
);
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError if status code is 4xx', async () => {
|
||||
testResponse.statusCode = 400;
|
||||
testResponse.body = { error: { message: 'Bad request' } };
|
||||
await expect(
|
||||
searchErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError if status code is 5xx', async () => {
|
||||
testResponse.statusCode = 500;
|
||||
testResponse.body = { error: { message: 'Server error' } };
|
||||
await expect(
|
||||
searchErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should include search API documentation in the description', async () => {
|
||||
testResponse.statusCode = 400;
|
||||
testResponse.body = { error: { message: 'Invalid query' } };
|
||||
await expect(
|
||||
searchErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
description: expect.stringContaining(
|
||||
'https://docs.perplexity.ai/api-reference/search-post',
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('embeddingsErrorPostReceive', () => {
|
||||
let testData: INodeExecutionData[];
|
||||
let testResponse: IN8nHttpFullResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
testData = [{ json: {} }];
|
||||
testResponse = { statusCode: 200, headers: {}, body: {} };
|
||||
});
|
||||
|
||||
it('should return data if status code is not 4xx or 5xx', async () => {
|
||||
const result = await embeddingsErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
testResponse,
|
||||
);
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError if status code is 4xx', async () => {
|
||||
testResponse.statusCode = 400;
|
||||
testResponse.body = { error: { message: 'Bad request' } };
|
||||
await expect(
|
||||
embeddingsErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError if status code is 5xx', async () => {
|
||||
testResponse.statusCode = 500;
|
||||
testResponse.body = { error: { message: 'Server error' } };
|
||||
await expect(
|
||||
embeddingsErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should include embeddings API documentation in the description', async () => {
|
||||
testResponse.statusCode = 400;
|
||||
testResponse.body = { error: { message: 'Invalid input' } };
|
||||
await expect(
|
||||
embeddingsErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
description: expect.stringContaining(
|
||||
'https://docs.perplexity.ai/api-reference/embeddings-post',
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Perplexity } from '../../Perplexity/Perplexity.node';
|
||||
import { description } from '../descriptions/chat/complete.operation';
|
||||
|
||||
jest.mock('../../Perplexity/GenericFunctions', () => ({
|
||||
getModels: jest.fn(),
|
||||
getAgentModels: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Perplexity Node', () => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
const credentials = {
|
||||
perplexityApi: {
|
||||
apiKey: 'test-api-key',
|
||||
baseUrl: 'https://api.perplexity.ai',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Perplexity Node - Agent Create Response', () => {
|
||||
beforeEach(() => {
|
||||
nock.disableNetConnect();
|
||||
nock('https://api.perplexity.ai')
|
||||
.post('/v1/agent', (body) => {
|
||||
return typeof body?.input === 'string';
|
||||
})
|
||||
.reply(200, {
|
||||
id: 'resp_test123',
|
||||
model: 'openai/gpt-5.2',
|
||||
object: 'response',
|
||||
status: 'completed',
|
||||
output: [
|
||||
{
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'output_text', text: '2+2 equals 4.' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-80, -680],
|
||||
"id": "a9105e3d-172b-411e-8c06-767a3ce1003a",
|
||||
"name": "When clicking 'Test workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "agent",
|
||||
"operation": "createResponse",
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"value": "openai/gpt-5.2",
|
||||
"mode": "id"
|
||||
},
|
||||
"input": "What is 2+2?",
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.perplexity",
|
||||
"typeVersion": 2,
|
||||
"position": [-40, -380],
|
||||
"id": "agent-node-1",
|
||||
"name": "Agent Response",
|
||||
"credentials": {
|
||||
"perplexityApi": {
|
||||
"id": "test",
|
||||
"name": "Perplexity account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking 'Test workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Agent Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Agent Response": [
|
||||
{
|
||||
"json": {
|
||||
"id": "resp_test123",
|
||||
"model": "openai/gpt-5.2",
|
||||
"object": "response",
|
||||
"status": "completed",
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{ "type": "output_text", "text": "2+2 equals 4." }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
const credentials = {
|
||||
perplexityApi: {
|
||||
apiKey: 'test-api-key',
|
||||
baseUrl: 'https://api.perplexity.ai',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Perplexity Node - Create Embedding', () => {
|
||||
beforeEach(() => {
|
||||
nock.disableNetConnect();
|
||||
nock('https://api.perplexity.ai')
|
||||
.post('/v1/embeddings', (body) => {
|
||||
return Array.isArray(body?.input) && typeof body?.model === 'string';
|
||||
})
|
||||
.reply(200, {
|
||||
object: 'list',
|
||||
data: [
|
||||
{
|
||||
object: 'embedding',
|
||||
index: 0,
|
||||
embedding: 'base64encodedstring...',
|
||||
},
|
||||
],
|
||||
model: 'pplx-embed-v1-4b',
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
total_tokens: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-80, -680],
|
||||
"id": "trigger-1",
|
||||
"name": "When clicking 'Test workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "embedding",
|
||||
"operation": "createEmbedding",
|
||||
"model": "pplx-embed-v1-4b",
|
||||
"input": "Hello world\nThis is a test",
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.perplexity",
|
||||
"typeVersion": 2,
|
||||
"position": [-40, -380],
|
||||
"id": "embed-node-1",
|
||||
"name": "Create Embedding",
|
||||
"credentials": {
|
||||
"perplexityApi": {
|
||||
"id": "test",
|
||||
"name": "Perplexity account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking 'Test workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Embedding",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Create Embedding": [
|
||||
{
|
||||
"json": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"object": "embedding",
|
||||
"index": 0,
|
||||
"embedding": "base64encodedstring..."
|
||||
}
|
||||
],
|
||||
"model": "pplx-embed-v1-4b",
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"total_tokens": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
const credentials = {
|
||||
perplexityApi: {
|
||||
apiKey: 'test-api-key',
|
||||
baseUrl: 'https://api.perplexity.ai',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Perplexity Node - Search', () => {
|
||||
beforeEach(() => {
|
||||
nock.disableNetConnect();
|
||||
nock('https://api.perplexity.ai')
|
||||
.post('/search', (body) => {
|
||||
return typeof body?.query === 'string';
|
||||
})
|
||||
.reply(200, {
|
||||
id: 'search-test123',
|
||||
results: [
|
||||
{
|
||||
title: 'AI Developments 2024',
|
||||
url: 'https://example.com/ai',
|
||||
snippet: 'Latest AI news...',
|
||||
date: '2024-12-15',
|
||||
last_updated: '2024-12-20',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-80, -680],
|
||||
"id": "trigger-1",
|
||||
"name": "When clicking 'Test workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "search",
|
||||
"operation": "search",
|
||||
"query": "latest AI developments",
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.perplexity",
|
||||
"typeVersion": 2,
|
||||
"position": [-40, -380],
|
||||
"id": "search-node-1",
|
||||
"name": "Search",
|
||||
"credentials": {
|
||||
"perplexityApi": {
|
||||
"id": "test",
|
||||
"name": "Perplexity account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking 'Test workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Search": [
|
||||
{
|
||||
"json": {
|
||||
"id": "search-test123",
|
||||
"results": [
|
||||
{
|
||||
"title": "AI Developments 2024",
|
||||
"url": "https://example.com/ai",
|
||||
"snippet": "Latest AI news...",
|
||||
"date": "2024-12-15",
|
||||
"last_updated": "2024-12-20"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user