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:
+106
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
validateNodeParameters,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { Content, GenerateContentResponse } from './interfaces';
|
||||
import { downloadFile, uploadFile } from './utils';
|
||||
import { apiRequest } from '../transport';
|
||||
|
||||
export async function baseAnalyze(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
urlsPropertyName: string,
|
||||
fallbackMimeType: string,
|
||||
): 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, {});
|
||||
validateNodeParameters(
|
||||
options,
|
||||
{ maxOutputTokens: { type: 'number', required: false } },
|
||||
this.getNode(),
|
||||
);
|
||||
const generationConfig = {
|
||||
maxOutputTokens: options.maxOutputTokens,
|
||||
};
|
||||
|
||||
let contents: Content[];
|
||||
if (inputType === 'url') {
|
||||
const urls = this.getNodeParameter(urlsPropertyName, i, '') as string;
|
||||
const filesDataPromises = urls
|
||||
.split(',')
|
||||
.map((url) => url.trim())
|
||||
.filter((url) => url)
|
||||
.map(async (url) => {
|
||||
if (url.startsWith('https://generativelanguage.googleapis.com')) {
|
||||
const { mimeType } = (await apiRequest.call(this, 'GET', '', {
|
||||
option: { url },
|
||||
})) as { mimeType: string };
|
||||
return { fileUri: url, mimeType };
|
||||
} else {
|
||||
const { fileContent, mimeType } = await downloadFile.call(this, url, fallbackMimeType);
|
||||
return await uploadFile.call(this, fileContent, mimeType);
|
||||
}
|
||||
});
|
||||
|
||||
const filesData = await Promise.all(filesDataPromises);
|
||||
contents = [
|
||||
{
|
||||
role: 'user',
|
||||
parts: filesData.map((fileData) => ({
|
||||
fileData,
|
||||
})),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
const binaryPropertyNames = this.getNodeParameter('binaryPropertyName', i, 'data');
|
||||
const promises = binaryPropertyNames
|
||||
.split(',')
|
||||
.map((binaryPropertyName) => binaryPropertyName.trim())
|
||||
.filter((binaryPropertyName) => binaryPropertyName)
|
||||
.map(async (binaryPropertyName) => {
|
||||
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
||||
const buffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
return await uploadFile.call(this, buffer, binaryData.mimeType);
|
||||
});
|
||||
|
||||
const filesData = await Promise.all(promises);
|
||||
contents = [
|
||||
{
|
||||
role: 'user',
|
||||
parts: filesData.map((fileData) => ({
|
||||
fileData,
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
contents[0].parts.push({ text });
|
||||
|
||||
const body = {
|
||||
contents,
|
||||
generationConfig,
|
||||
};
|
||||
|
||||
const response = (await apiRequest.call(this, 'POST', `/v1beta/${model}:generateContent`, {
|
||||
body,
|
||||
})) as GenerateContentResponse;
|
||||
|
||||
if (simplify) {
|
||||
return response.candidates.map((candidate) => ({
|
||||
json: candidate,
|
||||
pairedItem: { item: i },
|
||||
}));
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: { ...response },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import type {
|
||||
GenerateContentConfig,
|
||||
GenerationConfig,
|
||||
GenerateContentParameters,
|
||||
} from '@google/genai';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
export { Modality } from '@google/genai';
|
||||
|
||||
/* type created based on: https://ai.google.dev/api/generate-content#generationconfig */
|
||||
export type GenerateContentGenerationConfig = Pick<
|
||||
GenerationConfig,
|
||||
| 'stopSequences'
|
||||
| 'responseMimeType'
|
||||
| 'responseSchema'
|
||||
| 'responseJsonSchema'
|
||||
| 'responseModalities'
|
||||
| 'candidateCount'
|
||||
| 'maxOutputTokens'
|
||||
| 'temperature'
|
||||
| 'topP'
|
||||
| 'topK'
|
||||
| 'seed'
|
||||
| 'presencePenalty'
|
||||
| 'frequencyPenalty'
|
||||
| 'responseLogprobs'
|
||||
| 'logprobs'
|
||||
| 'speechConfig'
|
||||
| 'thinkingConfig'
|
||||
| 'mediaResolution'
|
||||
>;
|
||||
|
||||
/* Type created based on: https://ai.google.dev/api/generate-content#method:-models.streamgeneratecontent */
|
||||
export interface GenerateContentRequest extends IDataObject {
|
||||
contents: GenerateContentParameters['contents'];
|
||||
tools?: GenerateContentConfig['tools'];
|
||||
toolConfig?: GenerateContentConfig['toolConfig'];
|
||||
systemInstruction?: GenerateContentConfig['systemInstruction'];
|
||||
safetySettings?: GenerateContentConfig['safetySettings'];
|
||||
generationConfig?: GenerateContentGenerationConfig;
|
||||
cachedContent?: string;
|
||||
}
|
||||
|
||||
export interface GenerateContentResponse {
|
||||
candidates: Array<{
|
||||
content: Content;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Content {
|
||||
parts: Part[];
|
||||
role: string;
|
||||
}
|
||||
|
||||
export type Part =
|
||||
| { text: string }
|
||||
| {
|
||||
inlineData: {
|
||||
mimeType: string;
|
||||
data: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
functionCall: {
|
||||
id?: string;
|
||||
name: string;
|
||||
args?: IDataObject;
|
||||
};
|
||||
}
|
||||
| {
|
||||
functionResponse: {
|
||||
id?: string;
|
||||
name: string;
|
||||
response: IDataObject;
|
||||
};
|
||||
}
|
||||
| {
|
||||
fileData?: {
|
||||
mimeType?: string;
|
||||
fileUri?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface ImagenResponse {
|
||||
predictions: Array<{
|
||||
bytesBase64Encoded: string;
|
||||
mimeType: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface VeoResponse {
|
||||
name: string;
|
||||
done: boolean;
|
||||
error?: {
|
||||
message: string;
|
||||
};
|
||||
response: {
|
||||
generateVideoResponse: {
|
||||
generatedSamples: Array<{
|
||||
video: {
|
||||
uri: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* File Search operation interface for long-running upload operations
|
||||
* Based on: https://ai.google.dev/api/file-search/file-search-stores#method:-media.uploadtofilesearchstore
|
||||
*/
|
||||
export interface FileSearchOperation {
|
||||
name: string;
|
||||
done: boolean;
|
||||
error?: { message: string };
|
||||
response?: IDataObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* User configuration for built-in tools in the node parameters
|
||||
*/
|
||||
export interface BuiltInTools {
|
||||
googleSearch?: boolean;
|
||||
googleMaps?: {
|
||||
latitude?: number | string;
|
||||
longitude?: number | string;
|
||||
};
|
||||
urlContext?: boolean;
|
||||
fileSearch?: {
|
||||
fileSearchStoreNames?: string;
|
||||
metadataFilter?: string;
|
||||
};
|
||||
codeExecution?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool structure for the Google Gemini API request
|
||||
*/
|
||||
export interface Tool {
|
||||
functionDeclarations?: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: IDataObject;
|
||||
}>;
|
||||
googleSearch?: object;
|
||||
googleMaps?: object;
|
||||
urlContext?: object;
|
||||
fileSearch?: {
|
||||
fileSearchStoreNames?: string[];
|
||||
metadataFilter?: string;
|
||||
};
|
||||
codeExecution?: object;
|
||||
}
|
||||
+1071
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,327 @@
|
||||
import axios from 'axios';
|
||||
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import { Readable } from 'node:stream';
|
||||
import type Stream from 'node:stream';
|
||||
|
||||
import type { FileSearchOperation } from './interfaces';
|
||||
import { apiRequest } from '../transport';
|
||||
|
||||
const OPERATION_CHECK_INTERVAL = 1000;
|
||||
|
||||
interface File {
|
||||
name: string;
|
||||
uri: string;
|
||||
mimeType: string;
|
||||
state: string;
|
||||
error?: { message: string };
|
||||
}
|
||||
|
||||
interface FileStreamData {
|
||||
stream: Stream;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
interface FileBufferData {
|
||||
buffer: Buffer;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
interface UploadStreamConfig {
|
||||
endpoint: string;
|
||||
mimeType: string;
|
||||
body?: IDataObject;
|
||||
}
|
||||
|
||||
const CHUNK_SIZE = 256 * 1024;
|
||||
|
||||
export async function downloadFile(
|
||||
this: IExecuteFunctions,
|
||||
url: string,
|
||||
fallbackMimeType?: string,
|
||||
qs?: IDataObject,
|
||||
) {
|
||||
const downloadResponse = (await this.helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url,
|
||||
qs,
|
||||
returnFullResponse: true,
|
||||
encoding: 'arraybuffer',
|
||||
})) as { body: ArrayBuffer; headers: IDataObject };
|
||||
|
||||
const mimeType =
|
||||
(downloadResponse.headers?.['content-type'] as string)?.split(';')?.[0] ?? fallbackMimeType;
|
||||
const fileContent = Buffer.from(downloadResponse.body);
|
||||
return {
|
||||
fileContent,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
export async function uploadFile(this: IExecuteFunctions, fileContent: Buffer, mimeType: string) {
|
||||
const numBytes = fileContent.length.toString();
|
||||
const uploadInitResponse = (await apiRequest.call(this, 'POST', '/upload/v1beta/files', {
|
||||
headers: {
|
||||
'X-Goog-Upload-Protocol': 'resumable',
|
||||
'X-Goog-Upload-Command': 'start',
|
||||
'X-Goog-Upload-Header-Content-Length': numBytes,
|
||||
'X-Goog-Upload-Header-Content-Type': mimeType,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
option: {
|
||||
returnFullResponse: true,
|
||||
},
|
||||
})) as { headers: IDataObject };
|
||||
const uploadUrl = uploadInitResponse.headers['x-goog-upload-url'] as string;
|
||||
|
||||
const uploadResponse = (await this.helpers.httpRequest({
|
||||
method: 'POST',
|
||||
url: uploadUrl,
|
||||
headers: {
|
||||
'Content-Length': numBytes,
|
||||
'X-Goog-Upload-Offset': '0',
|
||||
'X-Goog-Upload-Command': 'upload, finalize',
|
||||
},
|
||||
body: fileContent,
|
||||
})) as { file: File };
|
||||
|
||||
while (uploadResponse.file.state !== 'ACTIVE' && uploadResponse.file.state !== 'FAILED') {
|
||||
await new Promise((resolve) => setTimeout(resolve, OPERATION_CHECK_INTERVAL));
|
||||
uploadResponse.file = (await apiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/v1beta/${uploadResponse.file.name}`,
|
||||
)) as File;
|
||||
}
|
||||
|
||||
if (uploadResponse.file.state === 'FAILED') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
uploadResponse.file.error?.message ?? 'Unknown error',
|
||||
{
|
||||
description: 'Error uploading file',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return { fileUri: uploadResponse.file.uri, mimeType: uploadResponse.file.mimeType };
|
||||
}
|
||||
|
||||
async function getFileStreamFromUrlOrBinary(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
downloadUrl?: string,
|
||||
fallbackMimeType?: string,
|
||||
qs?: IDataObject,
|
||||
): Promise<FileStreamData | FileBufferData> {
|
||||
if (downloadUrl) {
|
||||
const downloadResponse = await axios.get(downloadUrl, {
|
||||
params: qs,
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
const contentType = downloadResponse.headers['content-type'] as string | undefined;
|
||||
const mimeType = contentType?.split(';')?.[0] ?? fallbackMimeType ?? 'application/octet-stream';
|
||||
|
||||
return {
|
||||
stream: downloadResponse.data as Stream,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i, 'data');
|
||||
if (!binaryPropertyName) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Binary property name or download URL is required',
|
||||
{
|
||||
description: 'Error uploading file',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
||||
if (!binaryData.id) {
|
||||
const buffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
return {
|
||||
buffer,
|
||||
mimeType: binaryData.mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
stream: await this.helpers.getBinaryStream(binaryData.id, CHUNK_SIZE),
|
||||
mimeType: binaryData.mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadStream(
|
||||
this: IExecuteFunctions,
|
||||
stream: Stream,
|
||||
config: UploadStreamConfig,
|
||||
): Promise<{ body: IDataObject }> {
|
||||
const { endpoint, mimeType, body } = config;
|
||||
|
||||
const uploadInitResponse = (await apiRequest.call(this, 'POST', endpoint, {
|
||||
headers: {
|
||||
'X-Goog-Upload-Protocol': 'resumable',
|
||||
'X-Goog-Upload-Command': 'start',
|
||||
'X-Goog-Upload-Header-Content-Type': mimeType,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
option: { returnFullResponse: true },
|
||||
})) as { headers: IDataObject };
|
||||
|
||||
const uploadUrl = uploadInitResponse.headers['x-goog-upload-url'] as string;
|
||||
if (!uploadUrl) {
|
||||
throw new NodeOperationError(this.getNode(), 'Failed to get upload URL');
|
||||
}
|
||||
|
||||
return (await this.helpers.httpRequest({
|
||||
method: 'POST',
|
||||
url: uploadUrl,
|
||||
headers: {
|
||||
'X-Goog-Upload-Offset': '0',
|
||||
'X-Goog-Upload-Command': 'upload, finalize',
|
||||
'Content-Type': mimeType,
|
||||
},
|
||||
body: stream,
|
||||
returnFullResponse: true,
|
||||
})) as { body: IDataObject };
|
||||
}
|
||||
|
||||
export async function transferFile(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
downloadUrl?: string,
|
||||
fallbackMimeType?: string,
|
||||
qs?: IDataObject,
|
||||
) {
|
||||
const fileData = await getFileStreamFromUrlOrBinary.call(
|
||||
this,
|
||||
i,
|
||||
downloadUrl,
|
||||
fallbackMimeType,
|
||||
qs,
|
||||
);
|
||||
|
||||
if ('buffer' in fileData) {
|
||||
return await uploadFile.call(this, fileData.buffer, fileData.mimeType);
|
||||
}
|
||||
|
||||
const { stream, mimeType } = fileData;
|
||||
const uploadResponse = (await uploadStream.call(this, stream, {
|
||||
endpoint: '/upload/v1beta/files',
|
||||
mimeType,
|
||||
})) as { body: { file: File } };
|
||||
|
||||
let file = uploadResponse.body.file;
|
||||
|
||||
while (file.state !== 'ACTIVE' && file.state !== 'FAILED') {
|
||||
await new Promise((resolve) => setTimeout(resolve, OPERATION_CHECK_INTERVAL));
|
||||
file = (await apiRequest.call(this, 'GET', `/v1beta/${file.name}`)) as File;
|
||||
}
|
||||
|
||||
if (file.state === 'FAILED') {
|
||||
throw new NodeOperationError(this.getNode(), file.error?.message ?? 'Unknown error', {
|
||||
description: 'Error uploading file',
|
||||
});
|
||||
}
|
||||
|
||||
return { fileUri: file.uri, mimeType: file.mimeType };
|
||||
}
|
||||
|
||||
export async function createFileSearchStore(this: IExecuteFunctions, displayName: string) {
|
||||
return (await apiRequest.call(this, 'POST', '/v1beta/fileSearchStores', {
|
||||
body: { displayName },
|
||||
})) as IDataObject;
|
||||
}
|
||||
|
||||
export async function uploadToFileSearchStore(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
fileSearchStoreName: string,
|
||||
displayName: string,
|
||||
downloadUrl?: string,
|
||||
fallbackMimeType?: string,
|
||||
qs?: IDataObject,
|
||||
) {
|
||||
const fileData = await getFileStreamFromUrlOrBinary.call(
|
||||
this,
|
||||
i,
|
||||
downloadUrl,
|
||||
fallbackMimeType,
|
||||
qs,
|
||||
);
|
||||
|
||||
let stream: Stream;
|
||||
let mimeType: string;
|
||||
|
||||
if ('buffer' in fileData) {
|
||||
stream = Readable.from(fileData.buffer);
|
||||
mimeType = fileData.mimeType;
|
||||
} else {
|
||||
stream = fileData.stream;
|
||||
mimeType = fileData.mimeType;
|
||||
}
|
||||
|
||||
const uploadResponse = (await uploadStream.call(this, stream, {
|
||||
endpoint: `/upload/v1beta/${fileSearchStoreName}:uploadToFileSearchStore`,
|
||||
mimeType,
|
||||
body: { displayName, mimeType },
|
||||
})) as { body: { name: string } };
|
||||
|
||||
const operationName = uploadResponse.body.name;
|
||||
let operation = (await apiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/v1beta/${operationName}`,
|
||||
)) as FileSearchOperation;
|
||||
|
||||
while (!operation.done) {
|
||||
await new Promise((resolve) => setTimeout(resolve, OPERATION_CHECK_INTERVAL));
|
||||
operation = (await apiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/v1beta/${operationName}`,
|
||||
)) as FileSearchOperation;
|
||||
}
|
||||
|
||||
if (operation.error) {
|
||||
throw new NodeOperationError(this.getNode(), operation.error.message ?? 'Unknown error', {
|
||||
description: 'Error uploading file to File Search store',
|
||||
});
|
||||
}
|
||||
|
||||
return operation.response;
|
||||
}
|
||||
|
||||
export async function listFileSearchStores(
|
||||
this: IExecuteFunctions,
|
||||
pageSize?: number,
|
||||
pageToken?: string,
|
||||
) {
|
||||
const qs: IDataObject = {};
|
||||
if (pageSize !== undefined) {
|
||||
qs.pageSize = pageSize;
|
||||
}
|
||||
if (pageToken) {
|
||||
qs.pageToken = pageToken;
|
||||
}
|
||||
|
||||
return (await apiRequest.call(this, 'GET', '/v1beta/fileSearchStores', { qs })) as IDataObject;
|
||||
}
|
||||
|
||||
export async function deleteFileSearchStore(
|
||||
this: IExecuteFunctions,
|
||||
name: string,
|
||||
force?: boolean,
|
||||
) {
|
||||
const qs: IDataObject = {};
|
||||
if (force !== undefined) {
|
||||
qs.force = force;
|
||||
}
|
||||
|
||||
return (await apiRequest.call(this, 'DELETE', `/v1beta/${name}`, { qs })) as IDataObject;
|
||||
}
|
||||
Reference in New Issue
Block a user