first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -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-sonnet-4-5-20250929',
},
],
};
@@ -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');
}
@@ -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,
];
@@ -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 },
},
];
}
@@ -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 },
},
];
}
@@ -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,
];
@@ -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 },
}));
}
@@ -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,
},
},
];
}
@@ -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');
}
@@ -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,
];
@@ -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>;
@@ -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 },
},
];
}
@@ -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 },
},
];
}
@@ -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,
];
@@ -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 },
},
];
}
@@ -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');
});
});
@@ -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];
}
@@ -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,
];
@@ -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);
}
@@ -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', 'claude'],
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,
],
};