diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.test.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.test.ts new file mode 100644 index 00000000000..b748a9be242 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.test.ts @@ -0,0 +1,97 @@ +import { AIMessage } from '@langchain/core/messages' +import { convertMessageContentToParts } from './FlowiseChatGoogleGenerativeAI' + +describe('convertMessageContentToParts — Gemini "thinking" content blocks', () => { + it('round-trips a thinking content block back into a Gemini text Part with thought=true', () => { + const msg = new AIMessage({ + content: [ + { type: 'thinking', thinking: 'First I should validate the inputs.' } as any, + { type: 'text', text: 'OK, validated.' } + ] + }) + + const parts = convertMessageContentToParts(msg, false, []) + + const thoughtPart = parts.find((p: any) => p.thought === true) as any + expect(thoughtPart).toBeDefined() + expect(thoughtPart.text).toBe('First I should validate the inputs.') + expect(thoughtPart.thought).toBe(true) + // No signature in the input → no thoughtSignature in the output + expect(thoughtPart.thoughtSignature).toBeUndefined() + + const textPart = parts.find((p: any) => p.text === 'OK, validated.' && !p.thought) as any + expect(textPart).toBeDefined() + }) + + it('preserves the thoughtSignature for Gemini-3 multi-turn tool-call continuity', () => { + // signature is stored under `signature` on the LangChain block (see + // the response parsers' output in FlowiseChatGoogleGenerativeAI.ts); + // it must be emitted as `thoughtSignature` on the outgoing Part. + const msg = new AIMessage({ + content: [ + { + type: 'thinking', + thinking: 'Need to call the search tool.', + signature: 'abc123-thought-sig' + } as any + ] + }) + + const parts = convertMessageContentToParts(msg, false, []) + const p = parts[0] as any + expect(p.thought).toBe(true) + expect(p.thoughtSignature).toBe('abc123-thought-sig') + }) + + it('also accepts thoughtSignature on the LangChain block (alternate key name)', () => { + const msg = new AIMessage({ + content: [ + { + type: 'thinking', + thinking: 'alt', + thoughtSignature: 'sig-from-alt-key' + } as any + ] + }) + + const parts = convertMessageContentToParts(msg, false, []) + const p = parts[0] as any + expect(p.thought).toBe(true) + expect(p.thoughtSignature).toBe('sig-from-alt-key') + }) + + it('treats undefined/null thinking as empty text rather than throwing', () => { + // Producer sites in this file always emit a string `thinking`, + // but defensive: missing fields should not crash the converter + // — they should just degrade to an empty thought part. (`??` + // collapses both undefined and null into the fallback.) + const msg = new AIMessage({ + content: [{ type: 'thinking' } as any] // no `thinking` field at all + }) + const parts = convertMessageContentToParts(msg, false, []) + const p = parts[0] as any + expect(p.thought).toBe(true) + expect(p.text).toBe('') + }) + + it('fails fast on non-string thinking payload (per code-review feedback)', () => { + const msg = new AIMessage({ + content: [{ type: 'thinking', thinking: 42 } as any] + }) + expect(() => convertMessageContentToParts(msg, false, [])).toThrow(/Invalid 'thinking' content: expected string, got number/) + }) + + it('fails fast on non-string thinking signature', () => { + const msg = new AIMessage({ + content: [{ type: 'thinking', thinking: 'ok', signature: 42 } as any] + }) + expect(() => convertMessageContentToParts(msg, false, [])).toThrow(/Invalid 'thinking' signature: expected string, got number/) + }) + + it('still throws "Unknown content type" for truly unrecognized types', () => { + const msg = new AIMessage({ + content: [{ type: 'definitely-not-a-real-type', value: 1 } as any] + }) + expect(() => convertMessageContentToParts(msg, false, [])).toThrow(/Unknown content type/) + }) +}) diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts index 3c6086849c2..8395e75842c 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts @@ -302,6 +302,23 @@ function _convertLangChainContentToPart(content: MessageContentComplex, isMultim } } else if ('functionCall' in content) { return undefined + } else if (content.type === 'thinking') { + // Map LangChain `thinking` blocks back to Google's native shape: + // a text Part with `thought: true` and optional `thoughtSignature`. + // https://ai.google.dev/gemini-api/docs/thinking + const text = (content as any).thinking ?? (content as any).text + if (text !== undefined && typeof text !== 'string') { + throw new Error(`Invalid 'thinking' content: expected string, got ${typeof text}`) + } + const signature = (content as any).signature ?? (content as any).thoughtSignature + if (signature !== undefined && typeof signature !== 'string') { + throw new Error(`Invalid 'thinking' signature: expected string, got ${typeof signature}`) + } + return { + text: text ?? '', + thought: true, + ...(signature ? { thoughtSignature: signature } : {}) + } as Part } else { if ('type' in content) { throw new Error(`Unknown content type ${content.type}`)