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
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:
@@ -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 },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user