summaryrefslogtreecommitdiff
path: root/packages/core/src/utils/generateContentResponseUtilities.test.ts
diff options
context:
space:
mode:
authorJerop Kipruto <[email protected]>2025-06-15 01:48:01 -0400
committerGitHub <[email protected]>2025-06-15 01:48:01 -0400
commitab932ffaa535d9fee4872f5513a7551abc2ffe2a (patch)
tree55ca302df3dcdd1ba8c7f67c19df9f8043a40063 /packages/core/src/utils/generateContentResponseUtilities.test.ts
parente717c51aa1e46d1a1aabdb7e770c248b93e437a6 (diff)
Telemetry: Improve API response logging with function call details (#1064)
Diffstat (limited to 'packages/core/src/utils/generateContentResponseUtilities.test.ts')
-rw-r--r--packages/core/src/utils/generateContentResponseUtilities.test.ts323
1 files changed, 323 insertions, 0 deletions
diff --git a/packages/core/src/utils/generateContentResponseUtilities.test.ts b/packages/core/src/utils/generateContentResponseUtilities.test.ts
new file mode 100644
index 00000000..5dadab25
--- /dev/null
+++ b/packages/core/src/utils/generateContentResponseUtilities.test.ts
@@ -0,0 +1,323 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ getResponseText,
+ getResponseTextFromParts,
+ getFunctionCalls,
+ getFunctionCallsFromParts,
+ getFunctionCallsAsJson,
+ getFunctionCallsFromPartsAsJson,
+ getStructuredResponse,
+ getStructuredResponseFromParts,
+} from './generateContentResponseUtilities.js';
+import {
+ GenerateContentResponse,
+ Part,
+ FinishReason,
+ SafetyRating,
+} from '@google/genai';
+
+const mockTextPart = (text: string): Part => ({ text });
+const mockFunctionCallPart = (
+ name: string,
+ args?: Record<string, unknown>,
+): Part => ({
+ functionCall: { name, args: args ?? {} },
+});
+
+const mockResponse = (
+ parts: Part[],
+ finishReason: FinishReason = FinishReason.STOP,
+ safetyRatings: SafetyRating[] = [],
+): GenerateContentResponse => ({
+ candidates: [
+ {
+ content: {
+ parts,
+ role: 'model',
+ },
+ index: 0,
+ finishReason,
+ safetyRatings,
+ },
+ ],
+ promptFeedback: {
+ safetyRatings: [],
+ },
+ text: undefined,
+ data: undefined,
+ functionCalls: undefined,
+ executableCode: undefined,
+ codeExecutionResult: undefined,
+});
+
+const minimalMockResponse = (
+ candidates: GenerateContentResponse['candidates'],
+): GenerateContentResponse => ({
+ candidates,
+ promptFeedback: { safetyRatings: [] },
+ text: undefined,
+ data: undefined,
+ functionCalls: undefined,
+ executableCode: undefined,
+ codeExecutionResult: undefined,
+});
+
+describe('generateContentResponseUtilities', () => {
+ describe('getResponseText', () => {
+ it('should return undefined for no candidates', () => {
+ expect(getResponseText(minimalMockResponse(undefined))).toBeUndefined();
+ });
+ it('should return undefined for empty candidates array', () => {
+ expect(getResponseText(minimalMockResponse([]))).toBeUndefined();
+ });
+ it('should return undefined for no parts', () => {
+ const response = mockResponse([]);
+ expect(getResponseText(response)).toBeUndefined();
+ });
+ it('should extract text from a single text part', () => {
+ const response = mockResponse([mockTextPart('Hello')]);
+ expect(getResponseText(response)).toBe('Hello');
+ });
+ it('should concatenate text from multiple text parts', () => {
+ const response = mockResponse([
+ mockTextPart('Hello '),
+ mockTextPart('World'),
+ ]);
+ expect(getResponseText(response)).toBe('Hello World');
+ });
+ it('should ignore function call parts', () => {
+ const response = mockResponse([
+ mockTextPart('Hello '),
+ mockFunctionCallPart('testFunc'),
+ mockTextPart('World'),
+ ]);
+ expect(getResponseText(response)).toBe('Hello World');
+ });
+ it('should return undefined if only function call parts exist', () => {
+ const response = mockResponse([
+ mockFunctionCallPart('testFunc'),
+ mockFunctionCallPart('anotherFunc'),
+ ]);
+ expect(getResponseText(response)).toBeUndefined();
+ });
+ });
+
+ describe('getResponseTextFromParts', () => {
+ it('should return undefined for no parts', () => {
+ expect(getResponseTextFromParts([])).toBeUndefined();
+ });
+ it('should extract text from a single text part', () => {
+ expect(getResponseTextFromParts([mockTextPart('Hello')])).toBe('Hello');
+ });
+ it('should concatenate text from multiple text parts', () => {
+ expect(
+ getResponseTextFromParts([
+ mockTextPart('Hello '),
+ mockTextPart('World'),
+ ]),
+ ).toBe('Hello World');
+ });
+ it('should ignore function call parts', () => {
+ expect(
+ getResponseTextFromParts([
+ mockTextPart('Hello '),
+ mockFunctionCallPart('testFunc'),
+ mockTextPart('World'),
+ ]),
+ ).toBe('Hello World');
+ });
+ it('should return undefined if only function call parts exist', () => {
+ expect(
+ getResponseTextFromParts([
+ mockFunctionCallPart('testFunc'),
+ mockFunctionCallPart('anotherFunc'),
+ ]),
+ ).toBeUndefined();
+ });
+ });
+
+ describe('getFunctionCalls', () => {
+ it('should return undefined for no candidates', () => {
+ expect(getFunctionCalls(minimalMockResponse(undefined))).toBeUndefined();
+ });
+ it('should return undefined for empty candidates array', () => {
+ expect(getFunctionCalls(minimalMockResponse([]))).toBeUndefined();
+ });
+ it('should return undefined for no parts', () => {
+ const response = mockResponse([]);
+ expect(getFunctionCalls(response)).toBeUndefined();
+ });
+ it('should extract a single function call', () => {
+ const func = { name: 'testFunc', args: { a: 1 } };
+ const response = mockResponse([
+ mockFunctionCallPart(func.name, func.args),
+ ]);
+ expect(getFunctionCalls(response)).toEqual([func]);
+ });
+ it('should extract multiple function calls', () => {
+ const func1 = { name: 'testFunc1', args: { a: 1 } };
+ const func2 = { name: 'testFunc2', args: { b: 2 } };
+ const response = mockResponse([
+ mockFunctionCallPart(func1.name, func1.args),
+ mockFunctionCallPart(func2.name, func2.args),
+ ]);
+ expect(getFunctionCalls(response)).toEqual([func1, func2]);
+ });
+ it('should ignore text parts', () => {
+ const func = { name: 'testFunc', args: { a: 1 } };
+ const response = mockResponse([
+ mockTextPart('Some text'),
+ mockFunctionCallPart(func.name, func.args),
+ mockTextPart('More text'),
+ ]);
+ expect(getFunctionCalls(response)).toEqual([func]);
+ });
+ it('should return undefined if only text parts exist', () => {
+ const response = mockResponse([
+ mockTextPart('Some text'),
+ mockTextPart('More text'),
+ ]);
+ expect(getFunctionCalls(response)).toBeUndefined();
+ });
+ });
+
+ describe('getFunctionCallsFromParts', () => {
+ it('should return undefined for no parts', () => {
+ expect(getFunctionCallsFromParts([])).toBeUndefined();
+ });
+ it('should extract a single function call', () => {
+ const func = { name: 'testFunc', args: { a: 1 } };
+ expect(
+ getFunctionCallsFromParts([mockFunctionCallPart(func.name, func.args)]),
+ ).toEqual([func]);
+ });
+ it('should extract multiple function calls', () => {
+ const func1 = { name: 'testFunc1', args: { a: 1 } };
+ const func2 = { name: 'testFunc2', args: { b: 2 } };
+ expect(
+ getFunctionCallsFromParts([
+ mockFunctionCallPart(func1.name, func1.args),
+ mockFunctionCallPart(func2.name, func2.args),
+ ]),
+ ).toEqual([func1, func2]);
+ });
+ it('should ignore text parts', () => {
+ const func = { name: 'testFunc', args: { a: 1 } };
+ expect(
+ getFunctionCallsFromParts([
+ mockTextPart('Some text'),
+ mockFunctionCallPart(func.name, func.args),
+ mockTextPart('More text'),
+ ]),
+ ).toEqual([func]);
+ });
+ it('should return undefined if only text parts exist', () => {
+ expect(
+ getFunctionCallsFromParts([
+ mockTextPart('Some text'),
+ mockTextPart('More text'),
+ ]),
+ ).toBeUndefined();
+ });
+ });
+
+ describe('getFunctionCallsAsJson', () => {
+ it('should return JSON string of function calls', () => {
+ const func1 = { name: 'testFunc1', args: { a: 1 } };
+ const func2 = { name: 'testFunc2', args: { b: 2 } };
+ const response = mockResponse([
+ mockFunctionCallPart(func1.name, func1.args),
+ mockTextPart('text in between'),
+ mockFunctionCallPart(func2.name, func2.args),
+ ]);
+ const expectedJson = JSON.stringify([func1, func2], null, 2);
+ expect(getFunctionCallsAsJson(response)).toBe(expectedJson);
+ });
+ it('should return undefined if no function calls', () => {
+ const response = mockResponse([mockTextPart('Hello')]);
+ expect(getFunctionCallsAsJson(response)).toBeUndefined();
+ });
+ });
+
+ describe('getFunctionCallsFromPartsAsJson', () => {
+ it('should return JSON string of function calls from parts', () => {
+ const func1 = { name: 'testFunc1', args: { a: 1 } };
+ const func2 = { name: 'testFunc2', args: { b: 2 } };
+ const parts = [
+ mockFunctionCallPart(func1.name, func1.args),
+ mockTextPart('text in between'),
+ mockFunctionCallPart(func2.name, func2.args),
+ ];
+ const expectedJson = JSON.stringify([func1, func2], null, 2);
+ expect(getFunctionCallsFromPartsAsJson(parts)).toBe(expectedJson);
+ });
+ it('should return undefined if no function calls in parts', () => {
+ const parts = [mockTextPart('Hello')];
+ expect(getFunctionCallsFromPartsAsJson(parts)).toBeUndefined();
+ });
+ });
+
+ describe('getStructuredResponse', () => {
+ it('should return only text if only text exists', () => {
+ const response = mockResponse([mockTextPart('Hello World')]);
+ expect(getStructuredResponse(response)).toBe('Hello World');
+ });
+ it('should return only function call JSON if only function calls exist', () => {
+ const func = { name: 'testFunc', args: { data: 'payload' } };
+ const response = mockResponse([
+ mockFunctionCallPart(func.name, func.args),
+ ]);
+ const expectedJson = JSON.stringify([func], null, 2);
+ expect(getStructuredResponse(response)).toBe(expectedJson);
+ });
+ it('should return text and function call JSON if both exist', () => {
+ const text = 'Consider this data:';
+ const func = { name: 'processData', args: { item: 42 } };
+ const response = mockResponse([
+ mockTextPart(text),
+ mockFunctionCallPart(func.name, func.args),
+ ]);
+ const expectedJson = JSON.stringify([func], null, 2);
+ expect(getStructuredResponse(response)).toBe(`${text}\n${expectedJson}`);
+ });
+ it('should return undefined if neither text nor function calls exist', () => {
+ const response = mockResponse([]);
+ expect(getStructuredResponse(response)).toBeUndefined();
+ });
+ });
+
+ describe('getStructuredResponseFromParts', () => {
+ it('should return only text if only text exists in parts', () => {
+ const parts = [mockTextPart('Hello World')];
+ expect(getStructuredResponseFromParts(parts)).toBe('Hello World');
+ });
+ it('should return only function call JSON if only function calls exist in parts', () => {
+ const func = { name: 'testFunc', args: { data: 'payload' } };
+ const parts = [mockFunctionCallPart(func.name, func.args)];
+ const expectedJson = JSON.stringify([func], null, 2);
+ expect(getStructuredResponseFromParts(parts)).toBe(expectedJson);
+ });
+ it('should return text and function call JSON if both exist in parts', () => {
+ const text = 'Consider this data:';
+ const func = { name: 'processData', args: { item: 42 } };
+ const parts = [
+ mockTextPart(text),
+ mockFunctionCallPart(func.name, func.args),
+ ];
+ const expectedJson = JSON.stringify([func], null, 2);
+ expect(getStructuredResponseFromParts(parts)).toBe(
+ `${text}\n${expectedJson}`,
+ );
+ });
+ it('should return undefined if neither text nor function calls exist in parts', () => {
+ const parts: Part[] = [];
+ expect(getStructuredResponseFromParts(parts)).toBeUndefined();
+ });
+ });
+});