mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(Anthropic Node): New node (#17121)
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
1008
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/Anthropic.node.test.ts
vendored
Normal file
1008
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/Anthropic.node.test.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
17
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/Anthropic.node.ts
vendored
Normal file
17
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/Anthropic.node.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { IExecuteFunctions, INodeType } from 'n8n-workflow';
|
||||
|
||||
import { router } from './actions/router';
|
||||
import { versionDescription } from './actions/versionDescription';
|
||||
import { listSearch } from './methods';
|
||||
|
||||
export class Anthropic implements INodeType {
|
||||
description = versionDescription;
|
||||
|
||||
methods = {
|
||||
listSearch,
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
return await router.call(this);
|
||||
}
|
||||
}
|
||||
26
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/descriptions.ts
vendored
Normal file
26
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/descriptions.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const modelRLC: INodeProperties = {
|
||||
displayName: 'Model',
|
||||
name: 'modelId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'modelSearch',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. claude-3-5-sonnet-20241022',
|
||||
},
|
||||
],
|
||||
};
|
||||
103
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/document/analyze.operation.ts
vendored
Normal file
103
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/document/analyze.operation.ts
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { baseAnalyze } from '../../helpers/baseAnalyze';
|
||||
import { modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
modelRLC,
|
||||
{
|
||||
displayName: 'Text Input',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
placeholder: "e.g. What's in this document?",
|
||||
default: "What's in this document?",
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Type',
|
||||
name: 'inputType',
|
||||
type: 'options',
|
||||
default: 'url',
|
||||
options: [
|
||||
{
|
||||
name: 'Document URL(s)',
|
||||
value: 'url',
|
||||
},
|
||||
{
|
||||
name: 'Binary File(s)',
|
||||
value: 'binary',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'URL(s)',
|
||||
name: 'documentUrls',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. https://example.com/document.pdf',
|
||||
description:
|
||||
'URL(s) of the document(s) to analyze, multiple URLs can be added separated by comma',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputType: ['url'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Data Field Name(s)',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description:
|
||||
'Name of the binary field(s) which contains the document(s), seperate multiple field names with commas',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputType: ['binary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to simplify the response or not',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Length of Description (Max Tokens)',
|
||||
description: 'Fewer tokens will result in shorter, less detailed image description',
|
||||
name: 'maxTokens',
|
||||
type: 'number',
|
||||
default: 1024,
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['analyze'],
|
||||
resource: ['document'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
return await baseAnalyze.call(this, i, 'documentUrls', 'document');
|
||||
}
|
||||
29
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/document/index.ts
vendored
Normal file
29
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/document/index.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as analyze from './analyze.operation';
|
||||
|
||||
export { analyze };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Analyze Document',
|
||||
value: 'analyze',
|
||||
action: 'Analyze document',
|
||||
description: 'Take in documents and answer questions about them',
|
||||
},
|
||||
],
|
||||
default: 'analyze',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...analyze.description,
|
||||
];
|
||||
37
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/delete.operation.ts
vendored
Normal file
37
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/delete.operation.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../transport';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'File ID',
|
||||
name: 'fileId',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. file_123',
|
||||
description: 'ID of the file to delete',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['deleteFile'],
|
||||
resource: ['file'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const fileId = this.getNodeParameter('fileId', i, '') as string;
|
||||
const response = (await apiRequest.call(this, 'DELETE', `/v1/files/${fileId}`)) as {
|
||||
id: string;
|
||||
};
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
38
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/get.operation.ts
vendored
Normal file
38
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/get.operation.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import type { File } from '../../helpers/interfaces';
|
||||
import { getBaseUrl } from '../../helpers/utils';
|
||||
import { apiRequest } from '../../transport';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'File ID',
|
||||
name: 'fileId',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. file_123',
|
||||
description: 'ID of the file to get metadata for',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['get'],
|
||||
resource: ['file'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const fileId = this.getNodeParameter('fileId', i, '') as string;
|
||||
const baseUrl = await getBaseUrl.call(this);
|
||||
const response = (await apiRequest.call(this, 'GET', `/v1/files/${fileId}`)) as File;
|
||||
return [
|
||||
{
|
||||
json: { ...response, url: `${baseUrl}/v1/files/${response.id}` },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
53
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/index.ts
vendored
Normal file
53
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/index.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as deleteFile from './delete.operation';
|
||||
import * as get from './get.operation';
|
||||
import * as list from './list.operation';
|
||||
import * as upload from './upload.operation';
|
||||
|
||||
export { deleteFile, get, list, upload };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Upload File',
|
||||
value: 'upload',
|
||||
action: 'Upload a file',
|
||||
description: 'Upload a file to the Anthropic API for later use',
|
||||
},
|
||||
{
|
||||
name: 'Get File Metadata',
|
||||
value: 'get',
|
||||
action: 'Get file metadata',
|
||||
description: 'Get metadata for a file from the Anthropic API',
|
||||
},
|
||||
{
|
||||
name: 'List Files',
|
||||
value: 'list',
|
||||
action: 'List files',
|
||||
description: 'List files from the Anthropic API',
|
||||
},
|
||||
{
|
||||
name: 'Delete File',
|
||||
value: 'deleteFile',
|
||||
action: 'Delete a file',
|
||||
description: 'Delete a file from the Anthropic API',
|
||||
},
|
||||
],
|
||||
default: 'upload',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...deleteFile.description,
|
||||
...get.description,
|
||||
...list.description,
|
||||
...upload.description,
|
||||
];
|
||||
95
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/list.operation.ts
vendored
Normal file
95
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/list.operation.ts
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import type { File } from '../../helpers/interfaces';
|
||||
import { getBaseUrl } from '../../helpers/utils';
|
||||
import { apiRequest } from '../../transport';
|
||||
|
||||
interface FileListResponse {
|
||||
data: File[];
|
||||
first_id: string;
|
||||
last_id: string;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 1000,
|
||||
},
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['list'],
|
||||
resource: ['file'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnAll = this.getNodeParameter('returnAll', i, false);
|
||||
const limit = this.getNodeParameter('limit', i, 50);
|
||||
const baseUrl = await getBaseUrl.call(this);
|
||||
if (returnAll) {
|
||||
return await getAllFiles.call(this, baseUrl, i);
|
||||
} else {
|
||||
return await getFiles.call(this, baseUrl, i, limit);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllFiles(this: IExecuteFunctions, baseUrl: string, i: number) {
|
||||
let hasMore = true;
|
||||
let lastId: string | undefined = undefined;
|
||||
const files: File[] = [];
|
||||
while (hasMore) {
|
||||
const response = (await apiRequest.call(this, 'GET', '/v1/files', {
|
||||
qs: {
|
||||
limit: 1000,
|
||||
after_id: lastId,
|
||||
},
|
||||
})) as FileListResponse;
|
||||
|
||||
hasMore = response.has_more;
|
||||
lastId = response.last_id;
|
||||
files.push(...response.data);
|
||||
}
|
||||
|
||||
return files.map((file) => ({
|
||||
json: { ...file, url: `${baseUrl}/v1/files/${file.id}` },
|
||||
pairedItem: { item: i },
|
||||
}));
|
||||
}
|
||||
|
||||
async function getFiles(this: IExecuteFunctions, baseUrl: string, i: number, limit: number) {
|
||||
const response = (await apiRequest.call(this, 'GET', '/v1/files', {
|
||||
qs: {
|
||||
limit,
|
||||
},
|
||||
})) as FileListResponse;
|
||||
|
||||
return response.data.map((file) => ({
|
||||
json: { ...file, url: `${baseUrl}/v1/files/${file.id}` },
|
||||
pairedItem: { item: i },
|
||||
}));
|
||||
}
|
||||
103
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/upload.operation.ts
vendored
Normal file
103
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/file/upload.operation.ts
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import type { File } from '../../helpers/interfaces';
|
||||
import { downloadFile, getBaseUrl, uploadFile } from '../../helpers/utils';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Input Type',
|
||||
name: 'inputType',
|
||||
type: 'options',
|
||||
default: 'url',
|
||||
options: [
|
||||
{
|
||||
name: 'File URL',
|
||||
value: 'url',
|
||||
},
|
||||
{
|
||||
name: 'Binary File',
|
||||
value: 'binary',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'fileUrl',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. https://example.com/file.pdf',
|
||||
description: 'URL of the file to upload',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputType: ['url'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description: 'Name of the binary field which contains the file',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputType: ['binary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'fileName',
|
||||
type: 'string',
|
||||
description: 'The file name to use for the uploaded file',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['upload'],
|
||||
resource: ['file'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const inputType = this.getNodeParameter('inputType', i, 'url') as string;
|
||||
const fileName = this.getNodeParameter('options.fileName', i, 'file') as string;
|
||||
const baseUrl = await getBaseUrl.call(this);
|
||||
|
||||
let response: File;
|
||||
if (inputType === 'url') {
|
||||
const fileUrl = this.getNodeParameter('fileUrl', i, '') as string;
|
||||
const { fileContent, mimeType } = await downloadFile.call(this, fileUrl);
|
||||
response = await uploadFile.call(this, fileContent, mimeType, fileName);
|
||||
} else {
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i, 'data');
|
||||
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
||||
const buffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
response = await uploadFile.call(this, buffer, binaryData.mimeType, fileName);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: { ...response, url: `${baseUrl}/v1/files/${response.id}` },
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
102
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/image/analyze.operation.ts
vendored
Normal file
102
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/image/analyze.operation.ts
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { baseAnalyze } from '../../helpers/baseAnalyze';
|
||||
import { modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
modelRLC,
|
||||
{
|
||||
displayName: 'Text Input',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
placeholder: "e.g. What's in this image?",
|
||||
default: "What's in this image?",
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Type',
|
||||
name: 'inputType',
|
||||
type: 'options',
|
||||
default: 'url',
|
||||
options: [
|
||||
{
|
||||
name: 'Image URL(s)',
|
||||
value: 'url',
|
||||
},
|
||||
{
|
||||
name: 'Binary File(s)',
|
||||
value: 'binary',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'URL(s)',
|
||||
name: 'imageUrls',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. https://example.com/image.png',
|
||||
description: 'URL(s) of the image(s) to analyze, multiple URLs can be added separated by comma',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputType: ['url'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Data Field Name(s)',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description:
|
||||
'Name of the binary field(s) which contains the image(s), seperate multiple field names with commas',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputType: ['binary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to simplify the response or not',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Length of Description (Max Tokens)',
|
||||
description: 'Fewer tokens will result in shorter, less detailed image description',
|
||||
name: 'maxTokens',
|
||||
type: 'number',
|
||||
default: 1024,
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['analyze'],
|
||||
resource: ['image'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
return await baseAnalyze.call(this, i, 'imageUrls', 'image');
|
||||
}
|
||||
29
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/image/index.ts
vendored
Normal file
29
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/image/index.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as analyze from './analyze.operation';
|
||||
|
||||
export { analyze };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Analyze Image',
|
||||
value: 'analyze',
|
||||
action: 'Analyze image',
|
||||
description: 'Take in images and answer questions about them',
|
||||
},
|
||||
],
|
||||
default: 'analyze',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['image'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...analyze.description,
|
||||
];
|
||||
11
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/node.type.ts
vendored
Normal file
11
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/node.type.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { AllEntities } from 'n8n-workflow';
|
||||
|
||||
type NodeMap = {
|
||||
text: 'message';
|
||||
image: 'analyze';
|
||||
document: 'analyze';
|
||||
file: 'upload' | 'deleteFile' | 'get' | 'list';
|
||||
prompt: 'generate' | 'improve' | 'templatize';
|
||||
};
|
||||
|
||||
export type AnthropicType = AllEntities<NodeMap>;
|
||||
67
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/prompt/generate.operation.ts
vendored
Normal file
67
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/prompt/generate.operation.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import type { PromptResponse } from '../../helpers/interfaces';
|
||||
import { apiRequest } from '../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Task',
|
||||
name: 'task',
|
||||
type: 'string',
|
||||
description: "Description of the prompt's purpose",
|
||||
placeholder: 'e.g. A chef for a meal prep planning service',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['generate'],
|
||||
resource: ['prompt'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const task = this.getNodeParameter('task', i, '') as string;
|
||||
const simplify = this.getNodeParameter('simplify', i, true) as boolean;
|
||||
|
||||
const body = {
|
||||
task,
|
||||
};
|
||||
const response = (await apiRequest.call(this, 'POST', '/v1/experimental/generate_prompt', {
|
||||
body,
|
||||
enableAnthropicBetas: { promptTools: true },
|
||||
})) as PromptResponse;
|
||||
|
||||
if (simplify) {
|
||||
return [
|
||||
{
|
||||
json: {
|
||||
messages: response.messages,
|
||||
system: response.system,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: { ...response },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
135
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/prompt/improve.operation.ts
vendored
Normal file
135
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/prompt/improve.operation.ts
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import type { Message, PromptResponse } from '../../helpers/interfaces';
|
||||
import { apiRequest } from '../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Messages',
|
||||
name: 'messages',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
description: 'Messages that constitute the prompt to be improved',
|
||||
placeholder: 'Add Message',
|
||||
default: { values: [{ content: '', role: 'user' }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
description: 'The content of the message to be sent',
|
||||
default: '',
|
||||
placeholder: 'e.g. Concise instructions for a meal prep service',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Role',
|
||||
name: 'role',
|
||||
type: 'options',
|
||||
description:
|
||||
"Role in shaping the model's response, it tells the model how it should behave and interact with the user",
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
description: 'Send a message as a user and get a response from the model',
|
||||
},
|
||||
{
|
||||
name: 'Assistant',
|
||||
value: 'assistant',
|
||||
description: 'Tell the model to adopt a specific tone or personality',
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'System Message',
|
||||
name: 'system',
|
||||
type: 'string',
|
||||
description: 'The existing system prompt to incorporate, if any',
|
||||
default: '',
|
||||
placeholder: 'e.g. You are a professional meal prep chef',
|
||||
},
|
||||
{
|
||||
displayName: 'Feedback',
|
||||
name: 'feedback',
|
||||
type: 'string',
|
||||
description: 'Feedback for improving the prompt',
|
||||
default: '',
|
||||
placeholder: 'e.g. Make it more detailed and include cooking times',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['improve'],
|
||||
resource: ['prompt'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const messages = this.getNodeParameter('messages.values', i, []) as Message[];
|
||||
const simplify = this.getNodeParameter('simplify', i, true) as boolean;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const body = {
|
||||
messages,
|
||||
system: options.system,
|
||||
feedback: options.feedback,
|
||||
};
|
||||
const response = (await apiRequest.call(this, 'POST', '/v1/experimental/improve_prompt', {
|
||||
body,
|
||||
enableAnthropicBetas: { promptTools: true },
|
||||
})) as PromptResponse;
|
||||
|
||||
if (simplify) {
|
||||
return [
|
||||
{
|
||||
json: {
|
||||
messages: response.messages,
|
||||
system: response.system,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: { ...response },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
57
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/prompt/index.ts
vendored
Normal file
57
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/prompt/index.ts
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as generate from './generate.operation';
|
||||
import * as improve from './improve.operation';
|
||||
import * as templatize from './templatize.operation';
|
||||
|
||||
export { generate, improve, templatize };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Generate Prompt',
|
||||
value: 'generate',
|
||||
action: 'Generate a prompt',
|
||||
description: 'Generate a prompt for a model',
|
||||
},
|
||||
{
|
||||
name: 'Improve Prompt',
|
||||
value: 'improve',
|
||||
action: 'Improve a prompt',
|
||||
description: 'Improve a prompt for a model',
|
||||
},
|
||||
{
|
||||
name: 'Templatize Prompt',
|
||||
value: 'templatize',
|
||||
action: 'Templatize a prompt',
|
||||
description: 'Templatize a prompt for a model',
|
||||
},
|
||||
],
|
||||
default: 'generate',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['prompt'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'The <a href="https://docs.anthropic.com/en/api/prompt-tools-generate">prompt tools APIs</a> are in a closed research preview. Your organization must request access to use them.',
|
||||
name: 'experimentalNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['prompt'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...generate.description,
|
||||
...improve.description,
|
||||
...templatize.description,
|
||||
];
|
||||
127
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/prompt/templatize.operation.ts
vendored
Normal file
127
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/prompt/templatize.operation.ts
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import type { Message, TemplatizeResponse } from '../../helpers/interfaces';
|
||||
import { apiRequest } from '../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Messages',
|
||||
name: 'messages',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
description: 'Messages that constitute the prompt to be templatized',
|
||||
placeholder: 'Add Message',
|
||||
default: { values: [{ content: '', role: 'user' }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
description: 'The content of the message to be sent',
|
||||
default: '',
|
||||
placeholder: 'e.g. Translate hello to German',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Role',
|
||||
name: 'role',
|
||||
type: 'options',
|
||||
description:
|
||||
"Role in shaping the model's response, it tells the model how it should behave and interact with the user",
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
description: 'Send a message as a user and get a response from the model',
|
||||
},
|
||||
{
|
||||
name: 'Assistant',
|
||||
value: 'assistant',
|
||||
description: 'Tell the model to adopt a specific tone or personality',
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'System Message',
|
||||
name: 'system',
|
||||
type: 'string',
|
||||
description: 'The existing system prompt to templatize',
|
||||
default: '',
|
||||
placeholder: 'e.g. You are a professional English to German translator',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['templatize'],
|
||||
resource: ['prompt'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const messages = this.getNodeParameter('messages.values', i, []) as Message[];
|
||||
const simplify = this.getNodeParameter('simplify', i, true) as boolean;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const body = {
|
||||
messages,
|
||||
system: options.system,
|
||||
};
|
||||
const response = (await apiRequest.call(this, 'POST', '/v1/experimental/templatize_prompt', {
|
||||
body,
|
||||
enableAnthropicBetas: { promptTools: true },
|
||||
})) as TemplatizeResponse;
|
||||
|
||||
if (simplify) {
|
||||
return [
|
||||
{
|
||||
json: {
|
||||
messages: response.messages,
|
||||
system: response.system,
|
||||
variable_values: response.variable_values,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: { ...response },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
124
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/router.test.ts
vendored
Normal file
124
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/router.test.ts
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import * as document from './document';
|
||||
import * as file from './file';
|
||||
import * as image from './image';
|
||||
import * as prompt from './prompt';
|
||||
import { router } from './router';
|
||||
import * as text from './text';
|
||||
|
||||
describe('Anthropic router', () => {
|
||||
const mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
const mockDocument = jest.spyOn(document.analyze, 'execute');
|
||||
const mockFile = jest.spyOn(file.upload, 'execute');
|
||||
const mockImage = jest.spyOn(image.analyze, 'execute');
|
||||
const mockPrompt = jest.spyOn(prompt.generate, 'execute');
|
||||
const mockText = jest.spyOn(text.message, 'execute');
|
||||
const operationMocks = [
|
||||
[mockDocument, 'document', 'analyze'],
|
||||
[mockFile, 'file', 'upload'],
|
||||
[mockImage, 'image', 'analyze'],
|
||||
[mockText, 'text', 'message'],
|
||||
[mockPrompt, 'prompt', 'generate'],
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it.each(operationMocks)('should call the correct method', async (mock, resource, operation) => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameter) =>
|
||||
parameter === 'resource' ? resource : operation,
|
||||
);
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
},
|
||||
]);
|
||||
(mock as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
json: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await router.call(mockExecuteFunctions);
|
||||
|
||||
expect(mock).toHaveBeenCalledWith(0);
|
||||
expect(result).toEqual([[{ json: { foo: 'bar' } }]]);
|
||||
});
|
||||
|
||||
it('should return an error if the operation is not supported', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameter) =>
|
||||
parameter === 'resource' ? 'foo' : 'bar',
|
||||
);
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
|
||||
|
||||
await expect(router.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
'The operation "bar" is not supported!',
|
||||
);
|
||||
});
|
||||
|
||||
it('should loop over all items', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameter) =>
|
||||
parameter === 'resource' ? 'document' : 'analyze',
|
||||
);
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {
|
||||
text: 'item 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
text: 'item 2',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
text: 'item 3',
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockDocument.mockResolvedValueOnce([{ json: { response: 'foo' } }]);
|
||||
mockDocument.mockResolvedValueOnce([{ json: { response: 'bar' } }]);
|
||||
mockDocument.mockResolvedValueOnce([{ json: { response: 'baz' } }]);
|
||||
|
||||
const result = await router.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([
|
||||
[{ json: { response: 'foo' } }, { json: { response: 'bar' } }, { json: { response: 'baz' } }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should continue on fail', async () => {
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameter) =>
|
||||
parameter === 'resource' ? 'document' : 'analyze',
|
||||
);
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }, { json: {} }]);
|
||||
mockDocument.mockRejectedValue(new Error('Some error'));
|
||||
|
||||
const result = await router.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{ json: { error: 'Some error' }, pairedItem: { item: 0 } },
|
||||
{ json: { error: 'Some error' }, pairedItem: { item: 1 } },
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error if continueOnFail is false', async () => {
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameter) =>
|
||||
parameter === 'resource' ? 'document' : 'analyze',
|
||||
);
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
|
||||
mockDocument.mockRejectedValue(new Error('Some error'));
|
||||
|
||||
await expect(router.call(mockExecuteFunctions)).rejects.toThrow('Some error');
|
||||
});
|
||||
});
|
||||
64
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/router.ts
vendored
Normal file
64
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/router.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
import { NodeOperationError, type IExecuteFunctions, type INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import * as document from './document';
|
||||
import * as file from './file';
|
||||
import * as image from './image';
|
||||
import type { AnthropicType } from './node.type';
|
||||
import * as prompt from './prompt';
|
||||
import * as text from './text';
|
||||
|
||||
export async function router(this: IExecuteFunctions) {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
const anthropicTypeData = {
|
||||
resource,
|
||||
operation,
|
||||
} as AnthropicType;
|
||||
|
||||
let execute;
|
||||
switch (anthropicTypeData.resource) {
|
||||
case 'document':
|
||||
execute = document[anthropicTypeData.operation].execute;
|
||||
break;
|
||||
case 'file':
|
||||
execute = file[anthropicTypeData.operation].execute;
|
||||
break;
|
||||
case 'image':
|
||||
execute = image[anthropicTypeData.operation].execute;
|
||||
break;
|
||||
case 'prompt':
|
||||
execute = prompt[anthropicTypeData.operation].execute;
|
||||
break;
|
||||
case 'text':
|
||||
execute = text[anthropicTypeData.operation].execute;
|
||||
break;
|
||||
default:
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The operation "${operation}" is not supported!`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const responseData = await execute.call(this, i);
|
||||
returnData.push(...responseData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new NodeOperationError(this.getNode(), error, {
|
||||
itemIndex: i,
|
||||
description: error.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
29
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/text/index.ts
vendored
Normal file
29
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/text/index.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as message from './message.operation';
|
||||
|
||||
export { message };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Message a Model',
|
||||
value: 'message',
|
||||
action: 'Message a model',
|
||||
description: 'Create a completion with Anthropic model',
|
||||
},
|
||||
],
|
||||
default: 'message',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['text'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...message.description,
|
||||
];
|
||||
606
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/text/message.operation.ts
vendored
Normal file
606
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/text/message.operation.ts
vendored
Normal file
@@ -0,0 +1,606 @@
|
||||
import type { Tool } from '@langchain/core/tools';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError, updateDisplayOptions } from 'n8n-workflow';
|
||||
import zodToJsonSchema from 'zod-to-json-schema';
|
||||
|
||||
import { getConnectedTools } from '@utils/helpers';
|
||||
|
||||
import type {
|
||||
Content,
|
||||
File,
|
||||
Message,
|
||||
MessagesResponse,
|
||||
Tool as AnthropicTool,
|
||||
} from '../../helpers/interfaces';
|
||||
import {
|
||||
downloadFile,
|
||||
getBaseUrl,
|
||||
getMimeType,
|
||||
splitByComma,
|
||||
uploadFile,
|
||||
} from '../../helpers/utils';
|
||||
import { apiRequest } from '../../transport';
|
||||
import { modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
modelRLC,
|
||||
{
|
||||
displayName: 'Messages',
|
||||
name: 'messages',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Message',
|
||||
default: { values: [{ content: '', role: 'user' }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
description: 'The content of the message to be sent',
|
||||
default: '',
|
||||
placeholder: 'e.g. Hello, how can you help me?',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Role',
|
||||
name: 'role',
|
||||
type: 'options',
|
||||
description:
|
||||
"Role in shaping the model's response, it tells the model how it should behave and interact with the user",
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
description: 'Send a message as a user and get a response from the model',
|
||||
},
|
||||
{
|
||||
name: 'Assistant',
|
||||
value: 'assistant',
|
||||
description: 'Tell the model to adopt a specific tone or personality',
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Add Attachments',
|
||||
name: 'addAttachments',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to add attachments to the message',
|
||||
},
|
||||
{
|
||||
displayName: 'Attachments Input Type',
|
||||
name: 'attachmentsInputType',
|
||||
type: 'options',
|
||||
default: 'url',
|
||||
description: 'The type of input to use for the attachments',
|
||||
options: [
|
||||
{
|
||||
name: 'URL(s)',
|
||||
value: 'url',
|
||||
},
|
||||
{
|
||||
name: 'Binary File(s)',
|
||||
value: 'binary',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
addAttachments: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Attachment URL(s)',
|
||||
name: 'attachmentsUrls',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. https://example.com/image.png',
|
||||
description: 'URL(s) of the file(s) to attach, multiple URLs can be added separated by comma',
|
||||
displayOptions: {
|
||||
show: {
|
||||
addAttachments: [true],
|
||||
attachmentsInputType: ['url'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Attachment Input Data Field Name(s)',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
description:
|
||||
'Name of the binary field(s) which contains the file(s) to attach, multiple field names can be added separated by comma',
|
||||
displayOptions: {
|
||||
show: {
|
||||
addAttachments: [true],
|
||||
attachmentsInputType: ['binary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Include Merged Response',
|
||||
name: 'includeMergedResponse',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to include a single output string merging all text parts of the response',
|
||||
},
|
||||
{
|
||||
displayName: 'System Message',
|
||||
name: 'system',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. You are a helpful assistant',
|
||||
},
|
||||
{
|
||||
displayName: 'Code Execution',
|
||||
name: 'codeExecution',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to enable code execution. Not supported by all models.',
|
||||
},
|
||||
{
|
||||
displayName: 'Web Search',
|
||||
name: 'webSearch',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to enable web search',
|
||||
},
|
||||
{
|
||||
displayName: 'Web Search Max Uses',
|
||||
name: 'maxUses',
|
||||
type: 'number',
|
||||
default: 5,
|
||||
description: 'The maximum number of web search uses per request',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
numberPrecision: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Web Search Allowed Domains',
|
||||
name: 'allowedDomains',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Comma-separated list of domains to search. Only domains in this list will be searched. Conflicts with "Web Search Blocked Domains".',
|
||||
placeholder: 'e.g. google.com, wikipedia.org',
|
||||
},
|
||||
{
|
||||
displayName: 'Web Search Blocked Domains',
|
||||
name: 'blockedDomains',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Comma-separated list of domains to block from search. Conflicts with "Web Search Allowed Domains".',
|
||||
placeholder: 'e.g. google.com, wikipedia.org',
|
||||
},
|
||||
{
|
||||
displayName: 'Maximum Number of Tokens',
|
||||
name: 'maxTokens',
|
||||
default: 1024,
|
||||
description: 'The maximum number of tokens to generate in the completion',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
numberPrecision: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Randomness (Temperature)',
|
||||
name: 'temperature',
|
||||
default: 1,
|
||||
description:
|
||||
'Controls the randomness of the output. Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
numberPrecision: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Randomness (Top P)',
|
||||
name: 'topP',
|
||||
default: 0.7,
|
||||
description: 'The maximum cumulative probability of tokens to consider when sampling',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
numberPrecision: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Randomness (Top K)',
|
||||
name: 'topK',
|
||||
default: 5,
|
||||
description: 'The maximum number of tokens to consider when sampling',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
numberPrecision: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Max Tool Calls Iterations',
|
||||
name: 'maxToolsIterations',
|
||||
type: 'number',
|
||||
default: 15,
|
||||
description:
|
||||
'The maximum number of tool iteration cycles the LLM will run before stopping. A single iteration can contain multiple tool calls. Set to 0 for no limit',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
numberPrecision: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['message'],
|
||||
resource: ['text'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
interface MessageOptions {
|
||||
includeMergedResponse?: boolean;
|
||||
codeExecution?: boolean;
|
||||
webSearch?: boolean;
|
||||
allowedDomains?: string;
|
||||
blockedDomains?: string;
|
||||
maxUses?: number;
|
||||
maxTokens?: number;
|
||||
system?: string;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
topK?: number;
|
||||
}
|
||||
|
||||
function getFileTypeOrThrow(this: IExecuteFunctions, mimeType?: string): 'image' | 'document' {
|
||||
if (mimeType?.startsWith('image/')) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (mimeType === 'application/pdf') {
|
||||
return 'document';
|
||||
}
|
||||
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Unsupported file type: ${mimeType}. Only images and PDFs are supported.`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = this.getNodeParameter('modelId', i, '', { extractValue: true }) as string;
|
||||
const messages = this.getNodeParameter('messages.values', i, []) as Message[];
|
||||
const addAttachments = this.getNodeParameter('addAttachments', i, false) as boolean;
|
||||
const simplify = this.getNodeParameter('simplify', i, true) as boolean;
|
||||
const options = this.getNodeParameter('options', i, {}) as MessageOptions;
|
||||
|
||||
const { tools, connectedTools } = await getTools.call(this, options);
|
||||
|
||||
if (addAttachments) {
|
||||
if (options.codeExecution) {
|
||||
await addCodeAttachmentsToMessages.call(this, i, messages);
|
||||
} else {
|
||||
await addRegularAttachmentsToMessages.call(this, i, messages);
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
model,
|
||||
messages,
|
||||
tools,
|
||||
max_tokens: options.maxTokens ?? 1024,
|
||||
system: options.system,
|
||||
temperature: options.temperature,
|
||||
top_p: options.topP,
|
||||
top_k: options.topK,
|
||||
};
|
||||
|
||||
let response = (await apiRequest.call(this, 'POST', '/v1/messages', {
|
||||
body,
|
||||
enableAnthropicBetas: { codeExecution: options.codeExecution },
|
||||
})) as MessagesResponse;
|
||||
|
||||
const maxToolsIterations = this.getNodeParameter('options.maxToolsIterations', i, 15) as number;
|
||||
const abortSignal = this.getExecutionCancelSignal();
|
||||
let currentIteration = 0;
|
||||
let pauseTurns = 0;
|
||||
while (true) {
|
||||
if (abortSignal?.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.stop_reason === 'tool_use') {
|
||||
if (maxToolsIterations > 0 && currentIteration >= maxToolsIterations) {
|
||||
break;
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
});
|
||||
await handleToolUse.call(this, response, messages, connectedTools);
|
||||
currentIteration++;
|
||||
} else if (response.stop_reason === 'pause_turn') {
|
||||
// if the model has paused (can happen for the web search or code execution tool), we just retry 3 times
|
||||
if (pauseTurns >= 3) {
|
||||
break;
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
});
|
||||
pauseTurns++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
response = (await apiRequest.call(this, 'POST', '/v1/messages', {
|
||||
body,
|
||||
enableAnthropicBetas: { codeExecution: options.codeExecution },
|
||||
})) as MessagesResponse;
|
||||
}
|
||||
|
||||
const mergedResponse = options.includeMergedResponse
|
||||
? response.content
|
||||
.filter((c) => c.type === 'text')
|
||||
.map((c) => c.text)
|
||||
.join('')
|
||||
: undefined;
|
||||
|
||||
if (simplify) {
|
||||
return [
|
||||
{
|
||||
json: {
|
||||
content: response.content,
|
||||
merged_response: mergedResponse,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: { ...response, merged_response: mergedResponse },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function getTools(this: IExecuteFunctions, options: MessageOptions) {
|
||||
let connectedTools: Tool[] = [];
|
||||
const nodeInputs = this.getNodeInputs();
|
||||
// the node can be used as a tool, in this case there won't be any connected tools
|
||||
if (nodeInputs.some((i) => i.type === 'ai_tool')) {
|
||||
connectedTools = await getConnectedTools(this, true);
|
||||
}
|
||||
|
||||
const tools: AnthropicTool[] = connectedTools.map((t) => ({
|
||||
type: 'custom',
|
||||
name: t.name,
|
||||
input_schema: zodToJsonSchema(t.schema),
|
||||
description: t.description,
|
||||
}));
|
||||
|
||||
if (options.codeExecution) {
|
||||
tools.push({
|
||||
type: 'code_execution_20250522',
|
||||
name: 'code_execution',
|
||||
});
|
||||
}
|
||||
|
||||
if (options.webSearch) {
|
||||
const allowedDomains = options.allowedDomains
|
||||
? splitByComma(options.allowedDomains)
|
||||
: undefined;
|
||||
const blockedDomains = options.blockedDomains
|
||||
? splitByComma(options.blockedDomains)
|
||||
: undefined;
|
||||
tools.push({
|
||||
type: 'web_search_20250305',
|
||||
name: 'web_search',
|
||||
max_uses: options.maxUses,
|
||||
allowed_domains: allowedDomains,
|
||||
blocked_domains: blockedDomains,
|
||||
});
|
||||
}
|
||||
|
||||
return { tools, connectedTools };
|
||||
}
|
||||
|
||||
async function addCodeAttachmentsToMessages(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
messages: Message[],
|
||||
) {
|
||||
const inputType = this.getNodeParameter('attachmentsInputType', i, 'url') as string;
|
||||
const baseUrl = await getBaseUrl.call(this);
|
||||
const fileUrlPrefix = `${baseUrl}/v1/files/`;
|
||||
|
||||
let content: Content[];
|
||||
if (inputType === 'url') {
|
||||
const urls = this.getNodeParameter('attachmentsUrls', i, '') as string;
|
||||
const promises = splitByComma(urls).map(async (url) => {
|
||||
if (url.startsWith(fileUrlPrefix)) {
|
||||
return url.replace(fileUrlPrefix, '');
|
||||
} else {
|
||||
const { fileContent, mimeType } = await downloadFile.call(this, url);
|
||||
const response = await uploadFile.call(this, fileContent, mimeType);
|
||||
return response.id;
|
||||
}
|
||||
});
|
||||
|
||||
const fileIds = await Promise.all(promises);
|
||||
content = fileIds.map((fileId) => ({
|
||||
type: 'container_upload',
|
||||
file_id: fileId,
|
||||
}));
|
||||
} else {
|
||||
const binaryPropertyNames = this.getNodeParameter('binaryPropertyName', i, 'data');
|
||||
const promises = splitByComma(binaryPropertyNames).map(async (binaryPropertyName) => {
|
||||
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
||||
const buffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
const response = await uploadFile.call(this, buffer, binaryData.mimeType);
|
||||
return response.id;
|
||||
});
|
||||
|
||||
const fileIds = await Promise.all(promises);
|
||||
content = fileIds.map((fileId) => ({
|
||||
type: 'container_upload',
|
||||
file_id: fileId,
|
||||
}));
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
async function addRegularAttachmentsToMessages(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
messages: Message[],
|
||||
) {
|
||||
const inputType = this.getNodeParameter('attachmentsInputType', i, 'url') as string;
|
||||
const baseUrl = await getBaseUrl.call(this);
|
||||
const fileUrlPrefix = `${baseUrl}/v1/files/`;
|
||||
|
||||
let content: Content[];
|
||||
if (inputType === 'url') {
|
||||
const urls = this.getNodeParameter('attachmentsUrls', i, '') as string;
|
||||
const promises = splitByComma(urls).map(async (url) => {
|
||||
if (url.startsWith(fileUrlPrefix)) {
|
||||
const response = (await apiRequest.call(this, 'GET', '', {
|
||||
option: { url },
|
||||
})) as File;
|
||||
const type = getFileTypeOrThrow.call(this, response.mime_type);
|
||||
return {
|
||||
type,
|
||||
source: {
|
||||
type: 'file',
|
||||
file_id: url.replace(fileUrlPrefix, ''),
|
||||
},
|
||||
} as Content;
|
||||
} else {
|
||||
const response = (await this.helpers.httpRequest.call(this, {
|
||||
url,
|
||||
method: 'HEAD',
|
||||
returnFullResponse: true,
|
||||
})) as { headers: IDataObject };
|
||||
const mimeType = getMimeType(response.headers['content-type'] as string);
|
||||
const type = getFileTypeOrThrow.call(this, mimeType);
|
||||
return {
|
||||
type,
|
||||
source: {
|
||||
type: 'url',
|
||||
url,
|
||||
},
|
||||
} as Content;
|
||||
}
|
||||
});
|
||||
|
||||
content = await Promise.all(promises);
|
||||
} else {
|
||||
const binaryPropertyNames = this.getNodeParameter('binaryPropertyName', i, 'data');
|
||||
const promises = splitByComma(binaryPropertyNames).map(async (binaryPropertyName) => {
|
||||
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
||||
const type = getFileTypeOrThrow.call(this, binaryData.mimeType);
|
||||
const buffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
const fileBase64 = buffer.toString('base64');
|
||||
return {
|
||||
type,
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: binaryData.mimeType,
|
||||
data: fileBase64,
|
||||
},
|
||||
} as Content;
|
||||
});
|
||||
|
||||
content = await Promise.all(promises);
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleToolUse(
|
||||
this: IExecuteFunctions,
|
||||
response: MessagesResponse,
|
||||
messages: Message[],
|
||||
connectedTools: Tool[],
|
||||
) {
|
||||
const toolCalls = response.content.filter((c) => c.type === 'tool_use');
|
||||
if (!toolCalls.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolResults = {
|
||||
role: 'user' as const,
|
||||
content: [] as Content[],
|
||||
};
|
||||
for (const toolCall of toolCalls) {
|
||||
let toolResponse;
|
||||
for (const connectedTool of connectedTools) {
|
||||
if (connectedTool.name === toolCall.name) {
|
||||
toolResponse = (await connectedTool.invoke(toolCall.input)) as IDataObject;
|
||||
}
|
||||
}
|
||||
|
||||
toolResults.content.push({
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolCall.id,
|
||||
content:
|
||||
typeof toolResponse === 'object' ? JSON.stringify(toolResponse) : (toolResponse ?? ''),
|
||||
});
|
||||
}
|
||||
|
||||
messages.push(toolResults);
|
||||
}
|
||||
90
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/versionDescription.ts
vendored
Normal file
90
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/actions/versionDescription.ts
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import * as document from './document';
|
||||
import * as file from './file';
|
||||
import * as image from './image';
|
||||
import * as prompt from './prompt';
|
||||
import * as text from './text';
|
||||
|
||||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Anthropic',
|
||||
name: 'anthropic',
|
||||
icon: 'file:anthropic.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
|
||||
description: 'Interact with Anthropic AI models',
|
||||
defaults: {
|
||||
name: 'Anthropic',
|
||||
},
|
||||
usableAsTool: true,
|
||||
codex: {
|
||||
alias: ['LangChain', 'document', 'image', 'assistant'],
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Agents', 'Miscellaneous', 'Root Nodes'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-langchain.anthropic/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
inputs: `={{
|
||||
(() => {
|
||||
const resource = $parameter.resource;
|
||||
const operation = $parameter.operation;
|
||||
if (resource === 'text' && operation === 'message') {
|
||||
return [{ type: 'main' }, { type: 'ai_tool', displayName: 'Tools' }];
|
||||
}
|
||||
|
||||
return ['main'];
|
||||
})()
|
||||
}}`,
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'anthropicApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Document',
|
||||
value: 'document',
|
||||
},
|
||||
{
|
||||
name: 'File',
|
||||
value: 'file',
|
||||
},
|
||||
{
|
||||
name: 'Image',
|
||||
value: 'image',
|
||||
},
|
||||
{
|
||||
name: 'Prompt',
|
||||
value: 'prompt',
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
},
|
||||
],
|
||||
default: 'text',
|
||||
},
|
||||
...document.description,
|
||||
...file.description,
|
||||
...image.description,
|
||||
...prompt.description,
|
||||
...text.description,
|
||||
],
|
||||
};
|
||||
1
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/anthropic.svg
vendored
Normal file
1
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/anthropic.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="46" height="32" fill="none"><path fill="#7D7D87" d="M32.73 0h-6.945L38.45 32h6.945zM12.665 0 0 32h7.082l2.59-6.72h13.25l2.59 6.72h7.082L19.929 0zm-.702 19.337 4.334-11.246 4.334 11.246z"/></svg>
|
||||
|
After Width: | Height: | Size: 242 B |
97
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/helpers/baseAnalyze.ts
vendored
Normal file
97
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/helpers/baseAnalyze.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import type { Content, MessagesResponse } from './interfaces';
|
||||
import { getBaseUrl, splitByComma } from './utils';
|
||||
import { apiRequest } from '../transport';
|
||||
|
||||
export async function baseAnalyze(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
urlsPropertyName: string,
|
||||
type: 'image' | 'document',
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const model = this.getNodeParameter('modelId', i, '', { extractValue: true }) as string;
|
||||
const inputType = this.getNodeParameter('inputType', i, 'url') as string;
|
||||
const text = this.getNodeParameter('text', i, '') as string;
|
||||
const simplify = this.getNodeParameter('simplify', i, true) as boolean;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const baseUrl = await getBaseUrl.call(this);
|
||||
const fileUrlPrefix = `${baseUrl}/v1/files/`;
|
||||
|
||||
let content: Content[];
|
||||
if (inputType === 'url') {
|
||||
const urls = this.getNodeParameter(urlsPropertyName, i, '') as string;
|
||||
content = splitByComma(urls).map((url) => {
|
||||
if (url.startsWith(fileUrlPrefix)) {
|
||||
return {
|
||||
type,
|
||||
source: {
|
||||
type: 'file',
|
||||
file_id: url.replace(fileUrlPrefix, ''),
|
||||
},
|
||||
} as Content;
|
||||
} else {
|
||||
return {
|
||||
type,
|
||||
source: {
|
||||
type: 'url',
|
||||
url,
|
||||
},
|
||||
} as Content;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const binaryPropertyNames = this.getNodeParameter('binaryPropertyName', i, 'data');
|
||||
const promises = splitByComma(binaryPropertyNames).map(async (binaryPropertyName) => {
|
||||
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
||||
const buffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
const fileBase64 = buffer.toString('base64');
|
||||
return {
|
||||
type,
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: binaryData.mimeType,
|
||||
data: fileBase64,
|
||||
},
|
||||
} as Content;
|
||||
});
|
||||
|
||||
content = await Promise.all(promises);
|
||||
}
|
||||
|
||||
content.push({
|
||||
type: 'text',
|
||||
text,
|
||||
});
|
||||
|
||||
const body = {
|
||||
model,
|
||||
max_tokens: options.maxTokens ?? 1024,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = (await apiRequest.call(this, 'POST', '/v1/messages', {
|
||||
body,
|
||||
})) as MessagesResponse;
|
||||
|
||||
if (simplify) {
|
||||
return [
|
||||
{
|
||||
json: { content: response.content },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: { ...response },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
94
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/helpers/interfaces.ts
vendored
Normal file
94
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/helpers/interfaces.ts
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import type { JsonSchema7Type } from 'zod-to-json-schema';
|
||||
|
||||
export type FileSource =
|
||||
| {
|
||||
type: 'base64';
|
||||
media_type: string;
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
type: 'url';
|
||||
url: string;
|
||||
}
|
||||
| {
|
||||
type: 'file';
|
||||
file_id: string;
|
||||
};
|
||||
|
||||
export type Content =
|
||||
| {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: 'image';
|
||||
source: FileSource;
|
||||
}
|
||||
| {
|
||||
type: 'document';
|
||||
source: FileSource;
|
||||
}
|
||||
| {
|
||||
type: 'tool_use';
|
||||
id: string;
|
||||
name: string;
|
||||
input: IDataObject;
|
||||
}
|
||||
| {
|
||||
type: 'tool_result';
|
||||
tool_use_id: string;
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: 'container_upload';
|
||||
file_id: string;
|
||||
};
|
||||
|
||||
export interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
content: string | Content[];
|
||||
}
|
||||
|
||||
export interface File {
|
||||
created_at: string;
|
||||
downloadable: boolean;
|
||||
filename: string;
|
||||
id: string;
|
||||
mime_type: string;
|
||||
size_bytes: number;
|
||||
type: 'file';
|
||||
}
|
||||
|
||||
export type Tool =
|
||||
| {
|
||||
type: 'custom';
|
||||
name: string;
|
||||
input_schema: JsonSchema7Type;
|
||||
description: string;
|
||||
}
|
||||
| {
|
||||
type: 'web_search_20250305';
|
||||
name: 'web_search';
|
||||
max_uses?: number;
|
||||
allowed_domains?: string[];
|
||||
blocked_domains?: string[];
|
||||
}
|
||||
| {
|
||||
type: 'code_execution_20250522';
|
||||
name: 'code_execution';
|
||||
};
|
||||
|
||||
export interface MessagesResponse {
|
||||
content: Content[];
|
||||
stop_reason: string | null;
|
||||
}
|
||||
|
||||
export interface PromptResponse {
|
||||
messages: Message[];
|
||||
system: string;
|
||||
}
|
||||
|
||||
export interface TemplatizeResponse extends PromptResponse {
|
||||
variable_values: IDataObject;
|
||||
}
|
||||
196
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/helpers/utils.test.ts
vendored
Normal file
196
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/helpers/utils.test.ts
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { downloadFile, getBaseUrl, getMimeType, splitByComma, uploadFile } from './utils';
|
||||
import * as transport from '../transport';
|
||||
|
||||
describe('Anthropic -> utils', () => {
|
||||
const mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
const apiRequestMock = jest.spyOn(transport, 'apiRequest');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('getMimeType', () => {
|
||||
it('should extract mime type from content type string', () => {
|
||||
const result = getMimeType('application/pdf; q=0.9');
|
||||
expect(result).toBe('application/pdf');
|
||||
});
|
||||
|
||||
it('should return full string if no semicolon', () => {
|
||||
const result = getMimeType('application/pdf');
|
||||
expect(result).toBe('application/pdf');
|
||||
});
|
||||
|
||||
it('should return undefined for undefined input', () => {
|
||||
const result = getMimeType(undefined);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = getMimeType('');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadFile', () => {
|
||||
it('should download file', async () => {
|
||||
mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({
|
||||
body: new ArrayBuffer(10),
|
||||
headers: {
|
||||
'content-type': 'application/pdf',
|
||||
},
|
||||
});
|
||||
|
||||
const file = await downloadFile.call(mockExecuteFunctions, 'https://example.com/file.pdf');
|
||||
|
||||
expect(file).toEqual({
|
||||
fileContent: Buffer.from(new ArrayBuffer(10)),
|
||||
mimeType: 'application/pdf',
|
||||
});
|
||||
expect(mockExecuteFunctions.helpers.httpRequest).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
url: 'https://example.com/file.pdf',
|
||||
returnFullResponse: true,
|
||||
encoding: 'arraybuffer',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use fallback mime type if content type header is not present', async () => {
|
||||
mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({
|
||||
body: new ArrayBuffer(10),
|
||||
headers: {},
|
||||
});
|
||||
|
||||
const file = await downloadFile.call(mockExecuteFunctions, 'https://example.com/file.pdf');
|
||||
|
||||
expect(file).toEqual({
|
||||
fileContent: Buffer.from(new ArrayBuffer(10)),
|
||||
mimeType: 'application/octet-stream',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadFile', () => {
|
||||
it('should upload file', async () => {
|
||||
const fileContent = Buffer.from('test file content');
|
||||
const mimeType = 'text/plain';
|
||||
const fileName = 'test.txt';
|
||||
|
||||
apiRequestMock.mockResolvedValue({
|
||||
created_at: '2025-01-01T10:00:00Z',
|
||||
downloadable: true,
|
||||
filename: fileName,
|
||||
id: 'file_123',
|
||||
mime_type: mimeType,
|
||||
size_bytes: fileContent.length,
|
||||
type: 'file',
|
||||
});
|
||||
|
||||
const result = await uploadFile.call(mockExecuteFunctions, fileContent, mimeType, fileName);
|
||||
|
||||
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/files', {
|
||||
headers: expect.objectContaining({
|
||||
'content-type': expect.stringContaining('multipart/form-data'),
|
||||
}),
|
||||
body: expect.any(Object),
|
||||
});
|
||||
expect(result).toEqual({
|
||||
created_at: '2025-01-01T10:00:00Z',
|
||||
downloadable: true,
|
||||
filename: fileName,
|
||||
id: 'file_123',
|
||||
mime_type: mimeType,
|
||||
size_bytes: fileContent.length,
|
||||
type: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should upload file with default filename when not provided', async () => {
|
||||
const fileContent = Buffer.from('test file content');
|
||||
const mimeType = 'application/pdf';
|
||||
|
||||
apiRequestMock.mockResolvedValue({
|
||||
created_at: '2025-01-01T10:00:00Z',
|
||||
downloadable: true,
|
||||
filename: 'file',
|
||||
id: 'file_456',
|
||||
mime_type: mimeType,
|
||||
size_bytes: fileContent.length,
|
||||
type: 'file',
|
||||
});
|
||||
|
||||
const result = await uploadFile.call(mockExecuteFunctions, fileContent, mimeType);
|
||||
|
||||
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/files', {
|
||||
headers: expect.objectContaining({
|
||||
'content-type': expect.stringContaining('multipart/form-data'),
|
||||
}),
|
||||
body: expect.any(Object),
|
||||
});
|
||||
expect(result).toEqual({
|
||||
created_at: '2025-01-01T10:00:00Z',
|
||||
downloadable: true,
|
||||
filename: 'file',
|
||||
id: 'file_456',
|
||||
mime_type: mimeType,
|
||||
size_bytes: fileContent.length,
|
||||
type: 'file',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitByComma', () => {
|
||||
it('should split string by comma and trim', () => {
|
||||
const result = splitByComma('apple, banana, cherry');
|
||||
expect(result).toEqual(['apple', 'banana', 'cherry']);
|
||||
});
|
||||
|
||||
it('should handle string with extra spaces', () => {
|
||||
const result = splitByComma(' apple , banana , cherry ');
|
||||
expect(result).toEqual(['apple', 'banana', 'cherry']);
|
||||
});
|
||||
|
||||
it('should filter out empty strings', () => {
|
||||
const result = splitByComma('apple,, banana, , cherry,');
|
||||
expect(result).toEqual(['apple', 'banana', 'cherry']);
|
||||
});
|
||||
|
||||
it('should handle single item', () => {
|
||||
const result = splitByComma('apple');
|
||||
expect(result).toEqual(['apple']);
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = splitByComma('');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle string with only commas and spaces', () => {
|
||||
const result = splitByComma(' , , , ');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBaseUrl', () => {
|
||||
it('should return custom URL from credentials', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
url: 'https://custom-anthropic-api.com',
|
||||
});
|
||||
|
||||
const result = await getBaseUrl.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toBe('https://custom-anthropic-api.com');
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('anthropicApi');
|
||||
});
|
||||
|
||||
it('should return default URL when no custom URL in credentials', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({});
|
||||
|
||||
const result = await getBaseUrl.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toBe('https://api.anthropic.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
56
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/helpers/utils.ts
vendored
Normal file
56
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/helpers/utils.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
import FormData from 'form-data';
|
||||
import type { IDataObject, IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../transport';
|
||||
import type { File } from './interfaces';
|
||||
|
||||
export function getMimeType(contentType?: string) {
|
||||
return contentType?.split(';')?.[0];
|
||||
}
|
||||
|
||||
export async function downloadFile(this: IExecuteFunctions, url: string, qs?: IDataObject) {
|
||||
const downloadResponse = (await this.helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url,
|
||||
qs,
|
||||
returnFullResponse: true,
|
||||
encoding: 'arraybuffer',
|
||||
})) as { body: ArrayBuffer; headers: IDataObject };
|
||||
|
||||
const mimeType =
|
||||
getMimeType(downloadResponse.headers?.['content-type'] as string) ?? 'application/octet-stream';
|
||||
const fileContent = Buffer.from(downloadResponse.body);
|
||||
return {
|
||||
fileContent,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
export async function uploadFile(
|
||||
this: IExecuteFunctions,
|
||||
fileContent: Buffer,
|
||||
mimeType: string,
|
||||
fileName?: string,
|
||||
) {
|
||||
const form = new FormData();
|
||||
form.append('file', fileContent, {
|
||||
filename: fileName ?? 'file',
|
||||
contentType: mimeType,
|
||||
});
|
||||
return (await apiRequest.call(this, 'POST', '/v1/files', {
|
||||
headers: form.getHeaders(),
|
||||
body: form,
|
||||
})) as File;
|
||||
}
|
||||
|
||||
export function splitByComma(str: string) {
|
||||
return str
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s);
|
||||
}
|
||||
|
||||
export async function getBaseUrl(this: IExecuteFunctions | ILoadOptionsFunctions) {
|
||||
const credentials = await this.getCredentials('anthropicApi');
|
||||
return (credentials.url ?? 'https://api.anthropic.com') as string;
|
||||
}
|
||||
1
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/methods/index.ts
vendored
Normal file
1
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/methods/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * as listSearch from './listSearch';
|
||||
61
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/methods/listSearch.test.ts
vendored
Normal file
61
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/methods/listSearch.test.ts
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { modelSearch } from './listSearch';
|
||||
import * as transport from '../transport';
|
||||
|
||||
const mockResponse = {
|
||||
data: [
|
||||
{
|
||||
id: 'claude-opus-4-20250514',
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Anthropic -> listSearch', () => {
|
||||
const mockExecuteFunctions = mock<ILoadOptionsFunctions>();
|
||||
const apiRequestMock = jest.spyOn(transport, 'apiRequest');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('modelSearch', () => {
|
||||
it('should return all models', async () => {
|
||||
apiRequestMock.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await modelSearch.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
{
|
||||
name: 'claude-opus-4-20250514',
|
||||
value: 'claude-opus-4-20250514',
|
||||
},
|
||||
{
|
||||
name: 'claude-sonnet-4-20250514',
|
||||
value: 'claude-sonnet-4-20250514',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return filtered models', async () => {
|
||||
apiRequestMock.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await modelSearch.call(mockExecuteFunctions, 'sonnet');
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
{
|
||||
name: 'claude-sonnet-4-20250514',
|
||||
value: 'claude-sonnet-4-20250514',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
24
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/methods/listSearch.ts
vendored
Normal file
24
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/methods/listSearch.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../transport';
|
||||
|
||||
export async function modelSearch(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const response = (await apiRequest.call(this, 'GET', '/v1/models')) as {
|
||||
data: Array<{ id: string }>;
|
||||
};
|
||||
|
||||
let models = response.data;
|
||||
if (filter) {
|
||||
models = models.filter((model) => model.id.toLowerCase().includes(filter.toLowerCase()));
|
||||
}
|
||||
|
||||
return {
|
||||
results: models.map((model) => ({
|
||||
name: model.id,
|
||||
value: model.id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
166
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/transport/index.test.ts
vendored
Normal file
166
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/transport/index.test.ts
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import { apiRequest } from '.';
|
||||
|
||||
describe('Anthropic transport', () => {
|
||||
const executeFunctionsMock = mockDeep<IExecuteFunctions>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call httpRequestWithAuthentication with correct parameters', async () => {
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({
|
||||
url: 'https://custom-url.com',
|
||||
});
|
||||
|
||||
await apiRequest.call(executeFunctionsMock, 'GET', '/v1/messages', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
},
|
||||
qs: {
|
||||
test: 123,
|
||||
},
|
||||
});
|
||||
|
||||
expect(executeFunctionsMock.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'anthropicApi',
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'https://custom-url.com/v1/messages',
|
||||
json: true,
|
||||
body: {
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
},
|
||||
qs: {
|
||||
test: 123,
|
||||
},
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': 'files-api-2025-04-14',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the default url if no custom url is provided', async () => {
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({});
|
||||
|
||||
await apiRequest.call(executeFunctionsMock, 'GET', '/v1/messages');
|
||||
|
||||
expect(executeFunctionsMock.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'anthropicApi',
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'https://api.anthropic.com/v1/messages',
|
||||
json: true,
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': 'files-api-2025-04-14',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should override the values with `option`', async () => {
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({});
|
||||
|
||||
await apiRequest.call(executeFunctionsMock, 'GET', '', {
|
||||
option: {
|
||||
url: 'https://override-url.com',
|
||||
returnFullResponse: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(executeFunctionsMock.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'anthropicApi',
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'https://override-url.com',
|
||||
json: true,
|
||||
returnFullResponse: true,
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': 'files-api-2025-04-14',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should include prompt-tools beta when enableAnthropicBetas.promptTools is true', async () => {
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({});
|
||||
|
||||
await apiRequest.call(executeFunctionsMock, 'POST', '/v1/messages', {
|
||||
enableAnthropicBetas: {
|
||||
promptTools: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(executeFunctionsMock.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'anthropicApi',
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'https://api.anthropic.com/v1/messages',
|
||||
json: true,
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': 'files-api-2025-04-14,prompt-tools-2025-04-02',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should include code-execution beta when enableAnthropicBetas.codeExecution is true', async () => {
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({});
|
||||
|
||||
await apiRequest.call(executeFunctionsMock, 'POST', '/v1/messages', {
|
||||
enableAnthropicBetas: {
|
||||
codeExecution: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(executeFunctionsMock.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'anthropicApi',
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'https://api.anthropic.com/v1/messages',
|
||||
json: true,
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': 'files-api-2025-04-14,code-execution-2025-05-22',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should include both beta features when both are enabled', async () => {
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({});
|
||||
|
||||
await apiRequest.call(executeFunctionsMock, 'POST', '/v1/messages', {
|
||||
enableAnthropicBetas: {
|
||||
promptTools: true,
|
||||
codeExecution: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(executeFunctionsMock.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'anthropicApi',
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'https://api.anthropic.com/v1/messages',
|
||||
json: true,
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta':
|
||||
'files-api-2025-04-14,prompt-tools-2025-04-02,code-execution-2025-05-22',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
59
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/transport/index.ts
vendored
Normal file
59
packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/transport/index.ts
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import type FormData from 'form-data';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IHttpRequestMethods,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
type RequestParameters = {
|
||||
headers?: IDataObject;
|
||||
body?: IDataObject | string | FormData;
|
||||
qs?: IDataObject;
|
||||
option?: IDataObject;
|
||||
enableAnthropicBetas?: {
|
||||
promptTools?: boolean;
|
||||
codeExecution?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export async function apiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
parameters?: RequestParameters,
|
||||
) {
|
||||
const { body, qs, option, headers } = parameters ?? {};
|
||||
|
||||
const credentials = await this.getCredentials('anthropicApi');
|
||||
const baseUrl = credentials.url ?? 'https://api.anthropic.com';
|
||||
const url = `${baseUrl}${endpoint}`;
|
||||
|
||||
const betas = ['files-api-2025-04-14'];
|
||||
if (parameters?.enableAnthropicBetas?.promptTools) {
|
||||
betas.push('prompt-tools-2025-04-02');
|
||||
}
|
||||
|
||||
if (parameters?.enableAnthropicBetas?.codeExecution) {
|
||||
betas.push('code-execution-2025-05-22');
|
||||
}
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': betas.join(','),
|
||||
...headers,
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
url,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (option && Object.keys(option).length !== 0) {
|
||||
Object.assign(options, option);
|
||||
}
|
||||
|
||||
return await this.helpers.httpRequestWithAuthentication.call(this, 'anthropicApi', options);
|
||||
}
|
||||
@@ -48,6 +48,7 @@
|
||||
"dist/credentials/ZepApi.credentials.js"
|
||||
],
|
||||
"nodes": [
|
||||
"dist/nodes/vendors/Anthropic/Anthropic.node.js",
|
||||
"dist/nodes/vendors/GoogleGemini/GoogleGemini.node.js",
|
||||
"dist/nodes/vendors/OpenAi/OpenAi.node.js",
|
||||
"dist/nodes/agents/Agent/Agent.node.js",
|
||||
|
||||
Reference in New Issue
Block a user