diff --git a/packages/core/src/core/geminiRequest.test.ts b/packages/core/src/core/geminiRequest.test.ts deleted file mode 100644 index fd298cb6..00000000 --- a/packages/core/src/core/geminiRequest.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect } from 'vitest'; -import { partListUnionToString } from './geminiRequest.js'; -import { type Part } from '@google/genai'; - -describe('partListUnionToString', () => { - it('should return the string value if the input is a string', () => { - const result = partListUnionToString('hello'); - expect(result).toBe('hello'); - }); - - it('should return a concatenated string if the input is an array of strings', () => { - const result = partListUnionToString(['hello', ' ', 'world']); - expect(result).toBe('hello world'); - }); - - it('should handle videoMetadata', () => { - const part: Part = { videoMetadata: {} }; - const result = partListUnionToString(part); - expect(result).toBe('[Video Metadata]'); - }); - - it('should handle thought', () => { - const part: Part = { thought: true }; - const result = partListUnionToString(part); - expect(result).toBe('[Thought: true]'); - }); - - it('should handle codeExecutionResult', () => { - const part: Part = { codeExecutionResult: {} }; - const result = partListUnionToString(part); - expect(result).toBe('[Code Execution Result]'); - }); - - it('should handle executableCode', () => { - const part: Part = { executableCode: {} }; - const result = partListUnionToString(part); - expect(result).toBe('[Executable Code]'); - }); - - it('should handle fileData', () => { - const part: Part = { - fileData: { mimeType: 'text/plain', fileUri: 'file.txt' }, - }; - const result = partListUnionToString(part); - expect(result).toBe('[File Data]'); - }); - - it('should handle functionCall', () => { - const part: Part = { functionCall: { name: 'myFunction' } }; - const result = partListUnionToString(part); - expect(result).toBe('[Function Call: myFunction]'); - }); - - it('should handle functionResponse', () => { - const part: Part = { - functionResponse: { name: 'myFunction', response: {} }, - }; - const result = partListUnionToString(part); - expect(result).toBe('[Function Response: myFunction]'); - }); - - it('should handle inlineData', () => { - const part: Part = { inlineData: { mimeType: 'image/png', data: '...' } }; - const result = partListUnionToString(part); - expect(result).toBe(''); - }); - - it('should handle text', () => { - const part: Part = { text: 'hello' }; - const result = partListUnionToString(part); - expect(result).toBe('hello'); - }); - - it('should return an empty string for an unknown part type', () => { - const part: Part = {}; - const result = partListUnionToString(part); - expect(result).toBe(''); - }); -}); diff --git a/packages/core/src/core/geminiRequest.ts b/packages/core/src/core/geminiRequest.ts index e85bd51e..f3c52fbb 100644 --- a/packages/core/src/core/geminiRequest.ts +++ b/packages/core/src/core/geminiRequest.ts @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { type PartListUnion, type Part } from '@google/genai'; +import { type PartListUnion } from '@google/genai'; +import { partToString } from '../utils/partUtils.js'; /** * Represents a request to be sent to the Gemini API. @@ -14,58 +15,5 @@ import { type PartListUnion, type Part } from '@google/genai'; export type GeminiCodeRequest = PartListUnion; export function partListUnionToString(value: PartListUnion): string { - if (typeof value === 'string') { - return value; - } - - if (Array.isArray(value)) { - return value.map(partListUnionToString).join(''); - } - - // Cast to Part, assuming it might contain project-specific fields - const part = value as Part & { - videoMetadata?: unknown; - thought?: string; - codeExecutionResult?: unknown; - executableCode?: unknown; - }; - - if (part.videoMetadata !== undefined) { - return `[Video Metadata]`; - } - - if (part.thought !== undefined) { - return `[Thought: ${part.thought}]`; - } - - if (part.codeExecutionResult !== undefined) { - return `[Code Execution Result]`; - } - - if (part.executableCode !== undefined) { - return `[Executable Code]`; - } - - // Standard Part fields - if (part.fileData !== undefined) { - return `[File Data]`; - } - - if (part.functionCall !== undefined) { - return `[Function Call: ${part.functionCall.name}]`; - } - - if (part.functionResponse !== undefined) { - return `[Function Response: ${part.functionResponse.name}]`; - } - - if (part.inlineData !== undefined) { - return `<${part.inlineData.mimeType}>`; - } - - if (part.text !== undefined) { - return part.text; - } - - return ''; + return partToString(value, { verbose: true }); } diff --git a/packages/core/src/utils/partUtils.test.ts b/packages/core/src/utils/partUtils.test.ts new file mode 100644 index 00000000..eda85df2 --- /dev/null +++ b/packages/core/src/utils/partUtils.test.ts @@ -0,0 +1,166 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { partToString, getResponseText } from './partUtils.js'; +import { GenerateContentResponse, Part } from '@google/genai'; + +const mockResponse = ( + parts?: Array<{ text?: string; functionCall?: unknown }>, +): GenerateContentResponse => ({ + candidates: parts + ? [{ content: { parts: parts as Part[], role: 'model' }, index: 0 }] + : [], + promptFeedback: { safetyRatings: [] }, + text: undefined, + data: undefined, + functionCalls: undefined, + executableCode: undefined, + codeExecutionResult: undefined, +}); + +describe('partUtils', () => { + describe('partToString (default behavior)', () => { + it('should return empty string for undefined or null', () => { + // @ts-expect-error Testing invalid input + expect(partToString(undefined)).toBe(''); + // @ts-expect-error Testing invalid input + expect(partToString(null)).toBe(''); + }); + + it('should return string input unchanged', () => { + expect(partToString('hello')).toBe('hello'); + }); + + it('should concatenate strings from an array', () => { + expect(partToString(['a', 'b'])).toBe('ab'); + }); + + it('should return text property when provided a text part', () => { + expect(partToString({ text: 'hi' })).toBe('hi'); + }); + + it('should return empty string for non-text parts', () => { + const part: Part = { inlineData: { mimeType: 'image/png', data: '' } }; + expect(partToString(part)).toBe(''); + const part2: Part = { functionCall: { name: 'test' } }; + expect(partToString(part2)).toBe(''); + }); + }); + + describe('partToString (verbose)', () => { + const verboseOptions = { verbose: true }; + + it('should return empty string for undefined or null', () => { + // @ts-expect-error Testing invalid input + expect(partToString(undefined, verboseOptions)).toBe(''); + // @ts-expect-error Testing invalid input + expect(partToString(null, verboseOptions)).toBe(''); + }); + + it('should return string input unchanged', () => { + expect(partToString('hello', verboseOptions)).toBe('hello'); + }); + + it('should join parts if the value is an array', () => { + const parts = ['hello', { text: ' world' }]; + expect(partToString(parts, verboseOptions)).toBe('hello world'); + }); + + it('should return the text property if the part is an object with text', () => { + const part: Part = { text: 'hello world' }; + expect(partToString(part, verboseOptions)).toBe('hello world'); + }); + + it('should return descriptive string for videoMetadata part', () => { + const part = { videoMetadata: {} } as Part; + expect(partToString(part, verboseOptions)).toBe('[Video Metadata]'); + }); + + it('should return descriptive string for thought part', () => { + const part = { thought: 'thinking' } as unknown as Part; + expect(partToString(part, verboseOptions)).toBe('[Thought: thinking]'); + }); + + it('should return descriptive string for codeExecutionResult part', () => { + const part = { codeExecutionResult: {} } as Part; + expect(partToString(part, verboseOptions)).toBe( + '[Code Execution Result]', + ); + }); + + it('should return descriptive string for executableCode part', () => { + const part = { executableCode: {} } as Part; + expect(partToString(part, verboseOptions)).toBe('[Executable Code]'); + }); + + it('should return descriptive string for fileData part', () => { + const part = { fileData: {} } as Part; + expect(partToString(part, verboseOptions)).toBe('[File Data]'); + }); + + it('should return descriptive string for functionCall part', () => { + const part = { functionCall: { name: 'myFunction' } } as Part; + expect(partToString(part, verboseOptions)).toBe( + '[Function Call: myFunction]', + ); + }); + + it('should return descriptive string for functionResponse part', () => { + const part = { functionResponse: { name: 'myFunction' } } as Part; + expect(partToString(part, verboseOptions)).toBe( + '[Function Response: myFunction]', + ); + }); + + it('should return descriptive string for inlineData part', () => { + const part = { inlineData: { mimeType: 'image/png', data: '' } } as Part; + expect(partToString(part, verboseOptions)).toBe(''); + }); + + it('should return an empty string for an unknown part type', () => { + const part: Part = {}; + expect(partToString(part, verboseOptions)).toBe(''); + }); + + it('should handle complex nested arrays with various part types', () => { + const parts = [ + 'start ', + { text: 'middle' }, + [ + { functionCall: { name: 'func1' } }, + ' end', + { inlineData: { mimeType: 'audio/mp3', data: '' } }, + ], + ]; + expect(partToString(parts as Part, verboseOptions)).toBe( + 'start middle[Function Call: func1] end