diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/GoogleGemini.node.test.ts b/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/GoogleGemini.node.test.ts index a23c4fa3c210b..65a70d9506836 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/GoogleGemini.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/GoogleGemini.node.test.ts @@ -731,6 +731,54 @@ describe('GoogleGemini Node', () => { ); }); + it('should handle undefined entries in parts array', async () => { + executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { + switch (parameter) { + case 'modelId': + return 'models/gemini-2.5-flash'; + case 'messages.values': + return [{ role: 'user', content: 'foo' }]; + case 'simplify': + return true; + case 'jsonOutput': + return false; + case 'builtInTools': + return {}; + case 'options': + return {}; + case 'options.maxToolsIterations': + return 15; + default: + return undefined; + } + }); + executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]); + apiRequestMock.mockResolvedValue({ + candidates: [ + { + content: { + parts: [undefined, { text: 'bar' }], + role: 'model', + }, + }, + ], + }); + + const result = await text.message.execute.call(executeFunctionsMock, 0); + + expect(result).toEqual([ + { + json: { + content: { + parts: [undefined, { text: 'bar' }], + role: 'model', + }, + }, + pairedItem: { item: 0 }, + }, + ]); + }); + describe('includeMergedResponse', () => { it('should include mergedResponse per candidate when enabled and simplify is true', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/actions/text/message.operation.ts b/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/actions/text/message.operation.ts index 0d5ea801ee7ca..15d5aba32e1c3 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/actions/text/message.operation.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/actions/text/message.operation.ts @@ -1,4 +1,3 @@ -import { getConnectedTools } from '@utils/helpers'; import { type IDataObject, type IExecuteFunctions, @@ -11,6 +10,8 @@ import { } from 'n8n-workflow'; import zodToJsonSchema from 'zod-to-json-schema'; +import { getConnectedTools } from '@utils/helpers'; + import type { GenerateContentRequest, GenerateContentResponse, @@ -18,6 +19,8 @@ import type { Tool, GenerateContentGenerationConfig, BuiltInTools, + FunctionCallPart, + TextPart, } from '../../helpers/interfaces'; import { apiRequest } from '../../transport'; import { modelRLC } from '../descriptions'; @@ -346,8 +349,16 @@ const displayOptions = { export const description = updateDisplayOptions(displayOptions, properties); +function isFunctionCallPart(part: unknown): part is FunctionCallPart { + return !!part && typeof part === 'object' && 'functionCall' in part; +} + +function isTextPart(part: unknown): part is TextPart { + return !!part && typeof part === 'object' && 'text' in part; +} + function getToolCalls(response: GenerateContentResponse) { - return response.candidates.flatMap((c) => c.content.parts).filter((p) => 'functionCall' in p); + return response.candidates.flatMap((c) => c?.content?.parts ?? []).filter(isFunctionCallPart); } export async function execute(this: IExecuteFunctions, i: number): Promise { @@ -543,7 +554,10 @@ export async function execute(this: IExecuteFunctions, i: number): Promise c.content)); + contents.push.apply( + contents, + response.candidates.map((c) => c.content), + ); for (const { functionCall } of toolCalls) { let toolResponse; @@ -580,9 +594,9 @@ export async function execute(this: IExecuteFunctions, i: number): Promise ({ ...candidate, - mergedResponse: candidate.content.parts - .filter((part) => 'text' in part) - .map((part) => (part as { text: string }).text) + mergedResponse: (candidate?.content?.parts ?? []) + .filter(isTextPart) + .map((part) => part.text) .join(''), })) : response.candidates; diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/helpers/interfaces.ts b/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/helpers/interfaces.ts index cd70e1837e647..f41c6112fe4ca 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/helpers/interfaces.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/GoogleGemini/helpers/interfaces.ts @@ -51,34 +51,46 @@ export interface Content { role: string; } +export interface TextPart { + text: string; +} + +export interface InlineDataPart { + inlineData: { + mimeType: string; + data: string; + }; +} + +export interface FunctionCallPart { + functionCall: { + id?: string; + name: string; + args?: IDataObject; + }; +} + +export interface FunctionResponsePart { + functionResponse: { + id?: string; + name: string; + response: IDataObject; + }; +} + +export interface FileDataPart { + fileData?: { + mimeType?: string; + fileUri?: 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; - }; - }; + | TextPart + | InlineDataPart + | FunctionCallPart + | FunctionResponsePart + | FileDataPart; export interface ImagenResponse { predictions: Array<{