diff --git a/packages/nodes-base/credentials/PerplexityApi.credentials.ts b/packages/nodes-base/credentials/PerplexityApi.credentials.ts index 38ae401add6..12e014b5bfb 100644 --- a/packages/nodes-base/credentials/PerplexityApi.credentials.ts +++ b/packages/nodes-base/credentials/PerplexityApi.credentials.ts @@ -29,6 +29,8 @@ export class PerplexityApi implements ICredentialType { properties: { headers: { Authorization: '=Bearer {{$credentials.apiKey}}', + 'User-Agent': 'n8n', + 'X-Source': 'n8n', }, }, }; diff --git a/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts b/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts index 60014c877a2..19b0f0c4569 100644 --- a/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts @@ -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 { + 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 { + 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 }; +} diff --git a/packages/nodes-base/nodes/Perplexity/Perplexity.node.json b/packages/nodes-base/nodes/Perplexity/Perplexity.node.json index 59b0de7d650..f9bcbfbc101 100644 --- a/packages/nodes-base/nodes/Perplexity/Perplexity.node.json +++ b/packages/nodes-base/nodes/Perplexity/Perplexity.node.json @@ -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": [ { diff --git a/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts b/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts index c1aeff37cc4..4855791baf1 100644 --- a/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts +++ b/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts @@ -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, + }, + }; } diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/agent/Agent.resource.ts b/packages/nodes-base/nodes/Perplexity/descriptions/agent/Agent.resource.ts new file mode 100644 index 00000000000..8864bbf52b7 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/descriptions/agent/Agent.resource.ts @@ -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, +]; diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/agent/createResponse.operation.ts b/packages/nodes-base/nodes/Perplexity/descriptions/agent/createResponse.operation.ts new file mode 100644 index 00000000000..0cb03d111e3 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/descriptions/agent/createResponse.operation.ts @@ -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); diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/chat/complete.operation.ts b/packages/nodes-base/nodes/Perplexity/descriptions/chat/complete.operation.ts index ac62b8b4e75..602bead4454 100644 --- a/packages/nodes-base/nodes/Perplexity/descriptions/chat/complete.operation.ts +++ b/packages/nodes-base/nodes/Perplexity/descriptions/chat/complete.operation.ts @@ -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 - to the beginning of the domain string (e.g., -domain1). 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 - to the beginning of the domain string (e.g., -domain1). 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) }}', }, }, }, diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/Embeddings.resource.ts b/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/Embeddings.resource.ts new file mode 100644 index 00000000000..7fe936a80b4 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/Embeddings.resource.ts @@ -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, +]; diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/createContextualized.operation.ts b/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/createContextualized.operation.ts new file mode 100644 index 00000000000..51858ce7088 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/createContextualized.operation.ts @@ -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); diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/createEmbedding.operation.ts b/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/createEmbedding.operation.ts new file mode 100644 index 00000000000..fbdecc46171 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/descriptions/embeddings/createEmbedding.operation.ts @@ -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); diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/index.ts b/packages/nodes-base/nodes/Perplexity/descriptions/index.ts index 17087df57ed..a984440d05e 100644 --- a/packages/nodes-base/nodes/Perplexity/descriptions/index.ts +++ b/packages/nodes-base/nodes/Perplexity/descriptions/index.ts @@ -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'; diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/search/Search.resource.ts b/packages/nodes-base/nodes/Perplexity/descriptions/search/Search.resource.ts new file mode 100644 index 00000000000..bb5278a9a5f --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/descriptions/search/Search.resource.ts @@ -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, +]; diff --git a/packages/nodes-base/nodes/Perplexity/descriptions/search/search.operation.ts b/packages/nodes-base/nodes/Perplexity/descriptions/search/search.operation.ts new file mode 100644 index 00000000000..256e93db0ab --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/descriptions/search/search.operation.ts @@ -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); diff --git a/packages/nodes-base/nodes/Perplexity/test/GenericFunction.test.ts b/packages/nodes-base/nodes/Perplexity/test/GenericFunction.test.ts index a79cee1cf9c..3379889f436 100644 --- a/packages/nodes-base/nodes/Perplexity/test/GenericFunction.test.ts +++ b/packages/nodes-base/nodes/Perplexity/test/GenericFunction.test.ts @@ -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', + ), + }), + ); + }); + }); }); diff --git a/packages/nodes-base/nodes/Perplexity/test/Perplexity.node.test.ts b/packages/nodes-base/nodes/Perplexity/test/Perplexity.node.test.ts index 283b67f6e7b..1936043be13 100644 --- a/packages/nodes-base/nodes/Perplexity/test/Perplexity.node.test.ts +++ b/packages/nodes-base/nodes/Perplexity/test/Perplexity.node.test.ts @@ -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', () => { diff --git a/packages/nodes-base/nodes/Perplexity/test/agent/createResponse.test.ts b/packages/nodes-base/nodes/Perplexity/test/agent/createResponse.test.ts new file mode 100644 index 00000000000..7e413d1e787 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/test/agent/createResponse.test.ts @@ -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 }); +}); diff --git a/packages/nodes-base/nodes/Perplexity/test/agent/createResponse.workflow.json b/packages/nodes-base/nodes/Perplexity/test/agent/createResponse.workflow.json new file mode 100644 index 00000000000..72d0a1313ec --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/test/agent/createResponse.workflow.json @@ -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." }] + } + ] + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Perplexity/test/embeddings/createEmbedding.test.ts b/packages/nodes-base/nodes/Perplexity/test/embeddings/createEmbedding.test.ts new file mode 100644 index 00000000000..de80b7b96ca --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/test/embeddings/createEmbedding.test.ts @@ -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 }); +}); diff --git a/packages/nodes-base/nodes/Perplexity/test/embeddings/createEmbedding.workflow.json b/packages/nodes-base/nodes/Perplexity/test/embeddings/createEmbedding.workflow.json new file mode 100644 index 00000000000..a2d7ed15ba4 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/test/embeddings/createEmbedding.workflow.json @@ -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 + } + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Perplexity/test/search/search.test.ts b/packages/nodes-base/nodes/Perplexity/test/search/search.test.ts new file mode 100644 index 00000000000..da5504b4ab8 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/test/search/search.test.ts @@ -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 }); +}); diff --git a/packages/nodes-base/nodes/Perplexity/test/search/search.workflow.json b/packages/nodes-base/nodes/Perplexity/test/search/search.workflow.json new file mode 100644 index 00000000000..3f9f42511ef --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/test/search/search.workflow.json @@ -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" + } + ] + } + } + ] + } +}