mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(Perplexity Node): New node (#13604)
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import type {
|
||||
IExecuteSingleFunctions,
|
||||
IN8nHttpFullResponse,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { sendErrorPostReceive } from '../GenericFunctions';
|
||||
|
||||
// Mock implementation for `this` in `sendErrorPostReceive`
|
||||
const mockExecuteSingleFunctions = {
|
||||
getNode: () => ({
|
||||
name: 'Mock Node',
|
||||
type: 'mock-type',
|
||||
position: [0, 0],
|
||||
}),
|
||||
} as unknown as IExecuteSingleFunctions;
|
||||
|
||||
describe('Generic Functions', () => {
|
||||
describe('sendErrorPostReceive', () => {
|
||||
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 sendErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
testResponse,
|
||||
);
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError if status code is 4xx', async () => {
|
||||
testResponse.statusCode = 400;
|
||||
await expect(
|
||||
sendErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError if status code is 5xx', async () => {
|
||||
testResponse.statusCode = 500;
|
||||
await expect(
|
||||
sendErrorPostReceive.call(mockExecuteSingleFunctions, testData, testResponse),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError with "Invalid model" message if error type is invalid_model', async () => {
|
||||
const errorResponse = {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
error: {
|
||||
type: 'invalid_model',
|
||||
message: 'Invalid model type provided',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
errorResponse as unknown as IN8nHttpFullResponse,
|
||||
),
|
||||
).rejects.toThrowError(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, {
|
||||
message: 'Invalid model',
|
||||
description:
|
||||
'The model is not valid. Permitted models can be found in the documentation at https://docs.perplexity.ai/guides/model-cards.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NodeApiError with "Invalid parameter" message if error type is invalid_parameter', async () => {
|
||||
const errorResponse = {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
error: {
|
||||
type: 'invalid_parameter',
|
||||
message: 'Invalid parameter provided',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
errorResponse as unknown as IN8nHttpFullResponse,
|
||||
),
|
||||
).rejects.toThrowError(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, {
|
||||
message: 'Invalid parameter provided.',
|
||||
description:
|
||||
'Please check all input parameters and ensure they are correctly formatted. Valid values can be found in the documentation at https://docs.perplexity.ai/api-reference/chat-completions.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle "invalid_model" error with itemIndex', async () => {
|
||||
const errorResponse = {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
error: {
|
||||
type: 'invalid_model',
|
||||
message: 'Invalid model',
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
errorResponse as unknown as IN8nHttpFullResponse,
|
||||
),
|
||||
).rejects.toThrowError(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, {
|
||||
message: 'Invalid model',
|
||||
description: 'Permitted models documentation...',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle "invalid_parameter" error with non-string message', async () => {
|
||||
const errorResponse = {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
error: {
|
||||
type: 'invalid_parameter',
|
||||
message: { detail: 'Invalid param' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
errorResponse as unknown as IN8nHttpFullResponse,
|
||||
),
|
||||
).rejects.toThrowError(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, {
|
||||
message: 'An unexpected issue occurred.',
|
||||
description: 'Please check parameters...',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw generic error for unknown error type', async () => {
|
||||
const errorResponse = {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
error: {
|
||||
type: 'server_error',
|
||||
message: 'Internal server error',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
errorResponse as unknown as IN8nHttpFullResponse,
|
||||
),
|
||||
).rejects.toThrowError(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, {
|
||||
message: 'Internal server error.',
|
||||
description: 'Refer to API documentation...',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include itemIndex in error message when present', async () => {
|
||||
const errorResponse = {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
error: {
|
||||
type: 'other_error',
|
||||
message: 'Error with item',
|
||||
itemIndex: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendErrorPostReceive.call(
|
||||
mockExecuteSingleFunctions,
|
||||
testData,
|
||||
errorResponse as unknown as IN8nHttpFullResponse,
|
||||
),
|
||||
).rejects.toThrowError(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, {
|
||||
message: 'Error with item [Item 2].',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Perplexity } from '../../Perplexity/Perplexity.node';
|
||||
import { description } from '../descriptions/chat/complete.operation';
|
||||
|
||||
jest.mock('../../Perplexity/GenericFunctions', () => ({
|
||||
getModels: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Perplexity Node', () => {
|
||||
let node: Perplexity;
|
||||
|
||||
beforeEach(() => {
|
||||
node = new Perplexity();
|
||||
});
|
||||
|
||||
describe('Node Description', () => {
|
||||
it('should correctly include chat completion properties', () => {
|
||||
const properties = node.description.properties;
|
||||
|
||||
expect(properties).toEqual(expect.arrayContaining(description));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
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 - Chat Completions', () => {
|
||||
beforeEach(() => {
|
||||
nock.disableNetConnect();
|
||||
nock('https://api.perplexity.ai')
|
||||
.post('/chat/completions', (body) => {
|
||||
return (
|
||||
body?.model?.value === 'r1-1776' &&
|
||||
body?.model?.mode === 'id' &&
|
||||
Array.isArray(body?.messages) &&
|
||||
body.messages.length === 3 &&
|
||||
body.messages[0].role === 'user' &&
|
||||
body.messages[1].role === 'assistant' &&
|
||||
body.messages[2].role === 'user'
|
||||
);
|
||||
})
|
||||
.reply(200, {
|
||||
id: '6bb24c98-3071-4691-9a7b-dc4bc18c3c2c',
|
||||
model: 'r1-1776',
|
||||
created: 1743161086,
|
||||
object: 'chat.completion',
|
||||
usage: {
|
||||
prompt_tokens: 4,
|
||||
completion_tokens: 4,
|
||||
total_tokens: 8,
|
||||
},
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
finish_reason: 'length',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: '<think>\nOkay,',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-80, -680],
|
||||
"id": "a9105e3d-172b-411e-8c06-767a3ce1003a",
|
||||
"name": "When clicking ‘Test workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"value": "r1-1776",
|
||||
"mode": "id"
|
||||
},
|
||||
"messages": {
|
||||
"message": [
|
||||
{
|
||||
"content": "test"
|
||||
},
|
||||
{
|
||||
"content": "test",
|
||||
"role": "assistant"
|
||||
},
|
||||
{
|
||||
"content": "aaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"frequencyPenalty": 1,
|
||||
"maxTokens": 4,
|
||||
"temperature": 1.99,
|
||||
"topK": 4,
|
||||
"topP": 1,
|
||||
"presencePenalty": 2,
|
||||
"returnImages": false,
|
||||
"returnRelatedQuestions": false,
|
||||
"searchRecency": "month"
|
||||
},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.perplexity",
|
||||
"typeVersion": 1,
|
||||
"position": [-40, -380],
|
||||
"id": "8e8f857d-d773-449b-82bf-d96aabdb8c9f",
|
||||
"name": "Role Assistant and User",
|
||||
"credentials": {
|
||||
"perplexityApi": {
|
||||
"id": "zjtHjl6uQKo1V2rm",
|
||||
"name": "Perplexity account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Role Assistant and User",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Role Assistant and User": [
|
||||
{
|
||||
"json": {
|
||||
"id": "6bb24c98-3071-4691-9a7b-dc4bc18c3c2c",
|
||||
"model": "r1-1776",
|
||||
"created": 1743161086,
|
||||
"usage": {
|
||||
"prompt_tokens": 4,
|
||||
"completion_tokens": 4,
|
||||
"total_tokens": 8
|
||||
},
|
||||
"object": "chat.completion",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"finish_reason": "length",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "<think>\nOkay,"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { ICredentialDataDecryptedObject, IHttpRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { PerplexityApi } from '../../../../credentials/PerplexityApi.credentials';
|
||||
|
||||
describe('Perplexity API Credentials', () => {
|
||||
describe('authenticate', () => {
|
||||
const perplexityApi = new PerplexityApi();
|
||||
|
||||
it('should generate a valid authorization header', async () => {
|
||||
const credentials: ICredentialDataDecryptedObject = {
|
||||
apiKey: 'test-api-key',
|
||||
baseUrl: 'https://api.perplexity.ai',
|
||||
};
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://api.perplexity.ai/chat/completions',
|
||||
method: 'POST',
|
||||
body: {
|
||||
model: 'r1-1776',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json: true,
|
||||
};
|
||||
|
||||
const authProperty = perplexityApi.authenticate;
|
||||
|
||||
const result = {
|
||||
...requestOptions,
|
||||
headers: {
|
||||
...requestOptions.headers,
|
||||
Authorization: `Bearer ${credentials.apiKey}`,
|
||||
},
|
||||
};
|
||||
|
||||
expect(result.headers?.Authorization).toBe('Bearer test-api-key');
|
||||
|
||||
expect(authProperty.type).toBe('generic');
|
||||
});
|
||||
});
|
||||
|
||||
describe('test', () => {
|
||||
const perplexityApi = new PerplexityApi();
|
||||
|
||||
it('should have a valid test property', () => {
|
||||
expect(perplexityApi.test).toBeDefined();
|
||||
expect(perplexityApi.test.request).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user