From 2c2f5e063fc7639521d226a06beae8f19ceaacd6 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Sat, 1 Mar 2025 02:04:03 +0200 Subject: [PATCH 1/2] add node perplexity --- .../credentials/PerplexityApi.credentials.ts | 59 +++ .../Perplexity/ChatCompletionsDescription.ts | 336 ++++++++++++++++++ .../nodes/Perplexity/GenericFunctions.ts | 18 + .../nodes/Perplexity/Perplexity.node.json | 18 + .../nodes/Perplexity/Perplexity.node.ts | 48 +++ .../nodes/Perplexity/perplexity.svg | 35 ++ packages/nodes-base/package.json | 2 + 7 files changed, 516 insertions(+) create mode 100644 packages/nodes-base/credentials/PerplexityApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Perplexity/ChatCompletionsDescription.ts create mode 100644 packages/nodes-base/nodes/Perplexity/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Perplexity/Perplexity.node.json create mode 100644 packages/nodes-base/nodes/Perplexity/Perplexity.node.ts create mode 100644 packages/nodes-base/nodes/Perplexity/perplexity.svg diff --git a/packages/nodes-base/credentials/PerplexityApi.credentials.ts b/packages/nodes-base/credentials/PerplexityApi.credentials.ts new file mode 100644 index 0000000000..407add3eec --- /dev/null +++ b/packages/nodes-base/credentials/PerplexityApi.credentials.ts @@ -0,0 +1,59 @@ +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class PerplexityApi implements ICredentialType { + name = 'perplexityApi'; + + displayName = 'Perplexity API'; + + documentationUrl = 'https://docs.perplexity.ai/api-reference/chat-completions'; + + properties: INodeProperties[] = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string', + typeOptions: { password: true }, + required: true, + default: '', + description: 'Your Perplexity API key. Get it from your Perplexity account.', + }, + { + displayName: 'Base URL', + name: 'baseUrl', + type: 'string', + default: 'https://api.perplexity.ai', + description: 'The base URL for the Perplexity API. Defaults to "https://api.perplexity.ai".', + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '=Bearer {{$credentials.apiKey}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.baseUrl}}', + url: '/chat/completions', + method: 'POST', + body: { + model: 'r1-1776', + messages: [{ role: 'user', content: 'test' }], + }, + headers: { + Authorization: '=Bearer {{$credentials.apiKey}}', + 'Content-Type': 'application/json', + }, + json: true, + }, + }; +} diff --git a/packages/nodes-base/nodes/Perplexity/ChatCompletionsDescription.ts b/packages/nodes-base/nodes/Perplexity/ChatCompletionsDescription.ts new file mode 100644 index 0000000000..206aaa8047 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/ChatCompletionsDescription.ts @@ -0,0 +1,336 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import { sendErrorPostReceive } from './GenericFunctions'; + +export const chatCompletionsOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['chat'], + }, + }, + options: [ + { + name: 'Message a Model', + value: 'complete', + action: 'Message a model', + description: 'Create one or more completions for a given text', + routing: { + request: { + method: 'POST', + url: '/chat/completions', + }, + output: { postReceive: [sendErrorPostReceive] }, + }, + }, + ], + default: 'complete', + }, +]; + +export const chatCompletionsFields: INodeProperties[] = [ + { + displayName: 'Model', //TODO Fix resource locator to load the models + name: 'model', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + options: [ + { name: 'Sonar Deep Research', value: 'sonar-deep-research' }, + { name: 'Sonar Reasoning Pro', value: 'sonar-reasoning-pro' }, + { name: 'Sonar Reasoning', value: 'sonar-reasoning' }, + { name: 'Sonar Pro', value: 'sonar-pro' }, + { name: 'Sonar', value: 'sonar' }, + { name: 'R1-1776', value: 'r1-1776' }, + ], + searchable: true, + } as any, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + placeholder: 'e.g. sonar-deep-research', + }, + ], + description: 'The model which will generate the completion', + routing: { + send: { + type: 'body', + property: 'model', + }, + }, + }, + { + displayName: 'Messages', + name: 'messages', + type: 'fixedCollection', + required: true, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Message', + default: { + message: [ + { + role: 'user', + content: '', + }, + ], + }, + displayOptions: { + show: { + resource: ['chat'], + operation: ['complete'], + }, + }, + options: [ + { + displayName: 'Message', + name: 'message', + values: [ + { + displayName: 'Text', + name: 'content', + required: true, + type: 'string', + default: '', + description: 'The content of the message to be sent', + }, + { + displayName: 'Role', + name: 'role', + required: true, + type: 'options', + options: [ + { name: 'User', value: 'user' }, + { name: 'System', value: 'system' }, + { name: 'Assistant', value: 'assistant' }, + ], + default: 'user', + description: + "Role in shaping the model's response, it tells the model how it should behave and interact with the user", + }, + ], + }, + ], + routing: { + send: { + type: 'body', + property: 'messages', + value: '={{ $value.message }}', + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: ['chat'], + operation: ['complete'], + }, + }, + options: [ + { + displayName: 'Frequency Penalty', + name: 'frequency_penalty', + type: 'number', + default: 1, + description: + "Values greater than 1.0 penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim", + routing: { + send: { + type: 'body', + property: 'frequency_penalty', + }, + }, + }, + { + displayName: 'Maximum Number of Tokens', + name: 'max_tokens', + type: 'number', + default: 0, + description: + 'The maximum number of tokens to generate in the completion. The number of tokens requested plus the number of prompt tokens sent in messages must not exceed the context window token limit of model requested.', + routing: { + send: { + type: 'body', + property: 'max_tokens', + }, + }, + }, + { + displayName: 'Presence Penalty', + name: 'presence_penalty', + 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. 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: { + min: -2.0, + max: 2.0, + numberPrecision: 1, + }, + routing: { + send: { + type: 'body', + property: 'presence_penalty', + }, + }, + }, + { + displayName: 'Search Recency Filter', + name: 'search_recency', + type: 'options', + options: [ + { name: 'Hour', value: 'hour' }, + { name: 'Day', value: 'day' }, + { name: 'Week', value: 'week' }, + { name: 'Month', value: 'month' }, + ], + default: 'month', + description: 'Returns search results within the specified time interval', + routing: { + send: { + type: 'body', + property: 'search_recency', + }, + }, + }, + { + displayName: 'Output Randomness (Temperature)', + name: 'temperature', + type: 'number', + default: 0.2, + description: + 'The amount of randomness in the response, valued between 0 inclusive and 2 exclusive. Higher values are more random, and lower values are more deterministic.', + typeOptions: { + min: 0, + max: 2, + numberPrecision: 1, + }, + routing: { + send: { + type: 'body', + property: 'temperature', + }, + }, + }, + { + displayName: 'Output Randomness (Top K)', + name: 'top_k', + type: 'number', + default: 0, + description: + 'The number of tokens to keep for highest top-k filtering, specified as an integer between 0 and 2048 inclusive. If set to 0, top-k filtering is disabled. We recommend either altering Top K or Top P, but not both.', + typeOptions: { + min: 0, + max: 2048, + numberPrecision: 1, + }, + routing: { + send: { + type: 'body', + property: 'top_k', + }, + }, + }, + { + displayName: 'Output Randomness (Top P)', + name: 'top_p', + type: 'number', + default: 0.9, + description: + 'The nucleus sampling threshold, valued between 0 and 1 inclusive. For each subsequent token, the model considers the results of the tokens with top_p probability mass. We recommend either altering top_k or top_p, but not both.', + typeOptions: { + min: 0, + max: 1, + numberPrecision: 1, + }, + routing: { + send: { + type: 'body', + property: 'top_p', + }, + }, + }, + { + displayName: 'Return Images', + name: 'return_images', + type: 'boolean', + default: false, + description: + 'Whether determines 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: 'return_related_questions', + type: 'boolean', + default: false, + description: + 'Whether determines 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: 'search_domain_filter', + 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. Requires Perplexity API usage Tier-3.', + routing: { + send: { + type: 'body', + property: 'search_domain_filter', + }, + }, + }, + ], + }, + { + displayName: 'Simplify Output', + name: 'simplifyOutput', + type: 'boolean', + default: false, + description: 'Whether to return only essential fields (ID, citations, message)', + routing: { + output: { + postReceive: [ + { + type: 'set', + enabled: '={{$value}}', + properties: { + value: + '={{ { "id": $response.body?.id, "created": $response.body?.created, "citations": $response.body?.citations, "message": $response.body?.choices?.[0]?.message?.content } }}', + }, + }, + ], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts b/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts new file mode 100644 index 0000000000..4ef01acd49 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts @@ -0,0 +1,18 @@ +import type { + IExecuteSingleFunctions, + IN8nHttpFullResponse, + INodeExecutionData, + JsonObject, +} from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; + +export async function sendErrorPostReceive( + this: IExecuteSingleFunctions, + data: INodeExecutionData[], + response: IN8nHttpFullResponse, +): Promise { + if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject); + } + return data; +} diff --git a/packages/nodes-base/nodes/Perplexity/Perplexity.node.json b/packages/nodes-base/nodes/Perplexity/Perplexity.node.json new file mode 100644 index 0000000000..59b0de7d65 --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/Perplexity.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.perplexity", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Utility"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/credentials/perplexity/" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-langchain.perplexity/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts b/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts new file mode 100644 index 0000000000..cb7574d53b --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts @@ -0,0 +1,48 @@ +import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; +import { NodeConnectionType } from 'n8n-workflow'; + +import { chatCompletionsFields, chatCompletionsOperations } from './ChatCompletionsDescription'; + +export class Perplexity implements INodeType { + description: INodeTypeDescription = { + displayName: 'Perplexity', + name: 'perplexity', + icon: { light: 'file:perplexity.svg', dark: 'file:perplexity.dark.svg' }, + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Interact with the Perplexity API to generate AI responses with citations.', + defaults: { + name: 'Perplexity', + }, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'perplexityApi', + required: true, + }, + ], + requestDefaults: { + baseURL: '={{ $credentials.baseUrl ?? "https://api.perplexity.ai" }}', + ignoreHttpStatusErrors: true, + }, + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'hidden', + noDataExpression: true, + options: [ + { + name: 'Chat', + value: 'chat', + }, + ], + default: 'chat', + }, + ...chatCompletionsOperations, + ...chatCompletionsFields, + ], + }; +} diff --git a/packages/nodes-base/nodes/Perplexity/perplexity.svg b/packages/nodes-base/nodes/Perplexity/perplexity.svg new file mode 100644 index 0000000000..4789aabaca --- /dev/null +++ b/packages/nodes-base/nodes/Perplexity/perplexity.svg @@ -0,0 +1,35 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index db7ef72af3..29030418eb 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -267,6 +267,7 @@ "dist/credentials/PagerDutyOAuth2Api.credentials.js", "dist/credentials/PayPalApi.credentials.js", "dist/credentials/PeekalinkApi.credentials.js", + "dist/credentials/PerplexityApi.credentials.js", "dist/credentials/PhantombusterApi.credentials.js", "dist/credentials/PhilipsHueOAuth2Api.credentials.js", "dist/credentials/PipedriveApi.credentials.js", @@ -678,6 +679,7 @@ "dist/nodes/PayPal/PayPal.node.js", "dist/nodes/PayPal/PayPalTrigger.node.js", "dist/nodes/Peekalink/Peekalink.node.js", + "dist/nodes/Perplexity/Perplexity.node.js", "dist/nodes/Phantombuster/Phantombuster.node.js", "dist/nodes/PhilipsHue/PhilipsHue.node.js", "dist/nodes/Pipedrive/Pipedrive.node.js", From d99bd2fedc30642d707027c50057b20c1b3ba451 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Mon, 3 Mar 2025 22:34:17 +0200 Subject: [PATCH 2/2] add hardcoded model logic/ add model validation for max and min values --- .../Perplexity/ChatCompletionsDescription.ts | 50 ++++++++++--------- .../nodes/Perplexity/GenericFunctions.ts | 42 ++++++++++++++++ .../nodes/Perplexity/Perplexity.node.ts | 7 +++ 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/packages/nodes-base/nodes/Perplexity/ChatCompletionsDescription.ts b/packages/nodes-base/nodes/Perplexity/ChatCompletionsDescription.ts index 206aaa8047..8a3e6aa8d8 100644 --- a/packages/nodes-base/nodes/Perplexity/ChatCompletionsDescription.ts +++ b/packages/nodes-base/nodes/Perplexity/ChatCompletionsDescription.ts @@ -34,7 +34,7 @@ export const chatCompletionsOperations: INodeProperties[] = [ export const chatCompletionsFields: INodeProperties[] = [ { - displayName: 'Model', //TODO Fix resource locator to load the models + displayName: 'Model', name: 'model', type: 'resourceLocator', default: { mode: 'list', value: '' }, @@ -45,22 +45,25 @@ export const chatCompletionsFields: INodeProperties[] = [ name: 'list', type: 'list', typeOptions: { - options: [ - { name: 'Sonar Deep Research', value: 'sonar-deep-research' }, - { name: 'Sonar Reasoning Pro', value: 'sonar-reasoning-pro' }, - { name: 'Sonar Reasoning', value: 'sonar-reasoning' }, - { name: 'Sonar Pro', value: 'sonar-pro' }, - { name: 'Sonar', value: 'sonar' }, - { name: 'R1-1776', value: 'r1-1776' }, - ], + searchListMethod: 'getModels', searchable: true, - } as any, + }, }, { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. sonar-deep-research', + validation: [ + { + type: 'regex', + properties: { + regex: '^[a-zA-Z0-9-]+$', + errorMessage: + 'Not a valid Perplexity model ID. Model IDs must contain only alphanumeric characters and hyphens.', + }, + }, + ], }, ], description: 'The model which will generate the completion', @@ -150,6 +153,9 @@ export const chatCompletionsFields: INodeProperties[] = [ name: 'frequency_penalty', type: 'number', default: 1, + typeOptions: { + minValue: 1, + }, description: "Values greater than 1.0 penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim", routing: { @@ -181,9 +187,8 @@ export const chatCompletionsFields: INodeProperties[] = [ 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. 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: { - min: -2.0, - max: 2.0, - numberPrecision: 1, + minValue: -2.0, + maxValue: 2.0, }, routing: { send: { @@ -219,9 +224,8 @@ export const chatCompletionsFields: INodeProperties[] = [ description: 'The amount of randomness in the response, valued between 0 inclusive and 2 exclusive. Higher values are more random, and lower values are more deterministic.', typeOptions: { - min: 0, - max: 2, - numberPrecision: 1, + minValue: 0, + maxValue: 2, }, routing: { send: { @@ -238,9 +242,8 @@ export const chatCompletionsFields: INodeProperties[] = [ description: 'The number of tokens to keep for highest top-k filtering, specified as an integer between 0 and 2048 inclusive. If set to 0, top-k filtering is disabled. We recommend either altering Top K or Top P, but not both.', typeOptions: { - min: 0, - max: 2048, - numberPrecision: 1, + minValue: 0, + maxValue: 2048, }, routing: { send: { @@ -257,9 +260,8 @@ export const chatCompletionsFields: INodeProperties[] = [ description: 'The nucleus sampling threshold, valued between 0 and 1 inclusive. For each subsequent token, the model considers the results of the tokens with top_p probability mass. We recommend either altering top_k or top_p, but not both.', typeOptions: { - min: 0, - max: 1, - numberPrecision: 1, + minValue: 0, + maxValue: 1, }, routing: { send: { @@ -302,11 +304,13 @@ export const chatCompletionsFields: INodeProperties[] = [ 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. Requires Perplexity API usage Tier-3.', + '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()) }}', }, }, }, diff --git a/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts b/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts index 4ef01acd49..6c59d0b034 100644 --- a/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Perplexity/GenericFunctions.ts @@ -1,7 +1,10 @@ import type { IExecuteSingleFunctions, + ILoadOptionsFunctions, IN8nHttpFullResponse, INodeExecutionData, + INodeListSearchResult, + INodePropertyOptions, JsonObject, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; @@ -16,3 +19,42 @@ export async function sendErrorPostReceive( } return data; } +export async function getModels( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const models: INodePropertyOptions[] = [ + { + name: 'Sonar Deep Research', + value: 'sonar-deep-research', + }, + { + name: 'Sonar Reasoning Pro', + value: 'sonar-reasoning-pro', + }, + { + name: 'Sonar Reasoning', + value: 'sonar-reasoning', + }, + { + name: 'Sonar Pro', + value: 'sonar-pro', + }, + { + name: 'Sonar', + value: 'sonar', + }, + { + name: 'R1-1776', + value: 'r1-1776', + }, + ]; + + const filteredModels = filter + ? models.filter((model) => model.name.toLowerCase().includes(filter.toLowerCase())) + : models; + + return { + results: filteredModels, + }; +} diff --git a/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts b/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts index cb7574d53b..52fe2d6e58 100644 --- a/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts +++ b/packages/nodes-base/nodes/Perplexity/Perplexity.node.ts @@ -2,6 +2,7 @@ import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; import { chatCompletionsFields, chatCompletionsOperations } from './ChatCompletionsDescription'; +import { getModels } from './GenericFunctions'; export class Perplexity implements INodeType { description: INodeTypeDescription = { @@ -45,4 +46,10 @@ export class Perplexity implements INodeType { ...chatCompletionsFields, ], }; + + methods = { + listSearch: { + getModels, + }, + }; }