refactor(core): Centralize tool response formatting (#743)
This commit is contained in:
parent
4b2af10b04
commit
d179b3aae4
|
@ -11,12 +11,7 @@ import {
|
||||||
useReactToolScheduler,
|
useReactToolScheduler,
|
||||||
mapToDisplay,
|
mapToDisplay,
|
||||||
} from './useReactToolScheduler.js';
|
} from './useReactToolScheduler.js';
|
||||||
import {
|
import { PartUnion, FunctionResponse } from '@google/genai';
|
||||||
Part,
|
|
||||||
PartListUnion,
|
|
||||||
PartUnion,
|
|
||||||
FunctionResponse,
|
|
||||||
} from '@google/genai';
|
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
ToolCallRequestInfo,
|
ToolCallRequestInfo,
|
||||||
|
@ -26,7 +21,6 @@ import {
|
||||||
ToolCallConfirmationDetails,
|
ToolCallConfirmationDetails,
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
ToolCallResponseInfo,
|
ToolCallResponseInfo,
|
||||||
formatLlmContentForFunctionResponse, // Import from core
|
|
||||||
ToolCall, // Import from core
|
ToolCall, // Import from core
|
||||||
Status as ToolCallStatusType,
|
Status as ToolCallStatusType,
|
||||||
ApprovalMode, // Import from core
|
ApprovalMode, // Import from core
|
||||||
|
@ -93,120 +87,6 @@ const mockToolRequiresConfirmation: Tool = {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('formatLlmContentForFunctionResponse', () => {
|
|
||||||
it('should handle simple string llmContent', () => {
|
|
||||||
const llmContent = 'Simple text output';
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({ output: 'Simple text output' });
|
|
||||||
expect(additionalParts).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent as a single Part with text', () => {
|
|
||||||
const llmContent: Part = { text: 'Text from Part object' };
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({ output: 'Text from Part object' });
|
|
||||||
expect(additionalParts).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent as a PartListUnion array with a single text Part', () => {
|
|
||||||
const llmContent: PartListUnion = [{ text: 'Text from array' }];
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({ output: 'Text from array' });
|
|
||||||
expect(additionalParts).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent with inlineData', () => {
|
|
||||||
const llmContent: Part = {
|
|
||||||
inlineData: { mimeType: 'image/png', data: 'base64...' },
|
|
||||||
};
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({
|
|
||||||
status: 'Binary content of type image/png was processed.',
|
|
||||||
});
|
|
||||||
expect(additionalParts).toEqual([llmContent]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent with fileData', () => {
|
|
||||||
const llmContent: Part = {
|
|
||||||
fileData: { mimeType: 'application/pdf', fileUri: 'gs://...' },
|
|
||||||
};
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({
|
|
||||||
status: 'Binary content of type application/pdf was processed.',
|
|
||||||
});
|
|
||||||
expect(additionalParts).toEqual([llmContent]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent as an array of multiple Parts (text and inlineData)', () => {
|
|
||||||
const llmContent: PartListUnion = [
|
|
||||||
{ text: 'Some textual description' },
|
|
||||||
{ inlineData: { mimeType: 'image/jpeg', data: 'base64data...' } },
|
|
||||||
{ text: 'Another text part' },
|
|
||||||
];
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({
|
|
||||||
status: 'Tool execution succeeded.',
|
|
||||||
});
|
|
||||||
expect(additionalParts).toEqual(llmContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent as an array with a single inlineData Part', () => {
|
|
||||||
const llmContent: PartListUnion = [
|
|
||||||
{ inlineData: { mimeType: 'image/gif', data: 'gifdata...' } },
|
|
||||||
];
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({
|
|
||||||
status: 'Binary content of type image/gif was processed.',
|
|
||||||
});
|
|
||||||
expect(additionalParts).toEqual(llmContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent as a generic Part (not text, inlineData, or fileData)', () => {
|
|
||||||
const llmContent: Part = { functionCall: { name: 'test', args: {} } };
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({
|
|
||||||
status: 'Tool execution succeeded.',
|
|
||||||
});
|
|
||||||
expect(additionalParts).toEqual([llmContent]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty string llmContent', () => {
|
|
||||||
const llmContent = '';
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({ output: '' });
|
|
||||||
expect(additionalParts).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent as an empty array', () => {
|
|
||||||
const llmContent: PartListUnion = [];
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({
|
|
||||||
status: 'Tool execution succeeded.',
|
|
||||||
});
|
|
||||||
expect(additionalParts).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle llmContent as a Part with undefined inlineData/fileData/text', () => {
|
|
||||||
const llmContent: Part = {}; // An empty part object
|
|
||||||
const { functionResponseJson, additionalParts } =
|
|
||||||
formatLlmContentForFunctionResponse(llmContent);
|
|
||||||
expect(functionResponseJson).toEqual({
|
|
||||||
status: 'Tool execution succeeded.',
|
|
||||||
});
|
|
||||||
expect(additionalParts).toEqual([llmContent]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useReactToolScheduler in YOLO Mode', () => {
|
describe('useReactToolScheduler in YOLO Mode', () => {
|
||||||
let onComplete: Mock;
|
let onComplete: Mock;
|
||||||
let setPendingHistoryItem: Mock;
|
let setPendingHistoryItem: Mock;
|
||||||
|
@ -289,13 +169,13 @@ describe('useReactToolScheduler in YOLO Mode', () => {
|
||||||
request,
|
request,
|
||||||
response: expect.objectContaining({
|
response: expect.objectContaining({
|
||||||
resultDisplay: 'YOLO Formatted tool output',
|
resultDisplay: 'YOLO Formatted tool output',
|
||||||
responseParts: expect.arrayContaining([
|
responseParts: {
|
||||||
expect.objectContaining({
|
functionResponse: {
|
||||||
functionResponse: expect.objectContaining({
|
id: 'yoloCall',
|
||||||
response: { output: expectedOutput },
|
name: 'mockToolRequiresConfirmation',
|
||||||
}),
|
response: { output: expectedOutput },
|
||||||
}),
|
},
|
||||||
]),
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
@ -433,13 +313,13 @@ describe('useReactToolScheduler', () => {
|
||||||
request,
|
request,
|
||||||
response: expect.objectContaining({
|
response: expect.objectContaining({
|
||||||
resultDisplay: 'Formatted tool output',
|
resultDisplay: 'Formatted tool output',
|
||||||
responseParts: expect.arrayContaining([
|
responseParts: {
|
||||||
expect.objectContaining({
|
functionResponse: {
|
||||||
functionResponse: expect.objectContaining({
|
id: 'call1',
|
||||||
response: { output: 'Tool output' },
|
name: 'mockTool',
|
||||||
}),
|
response: { output: 'Tool output' },
|
||||||
}),
|
},
|
||||||
]),
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
@ -917,13 +797,13 @@ describe('useReactToolScheduler', () => {
|
||||||
request: requests[0],
|
request: requests[0],
|
||||||
response: expect.objectContaining({
|
response: expect.objectContaining({
|
||||||
resultDisplay: 'Display 1',
|
resultDisplay: 'Display 1',
|
||||||
responseParts: expect.arrayContaining([
|
responseParts: {
|
||||||
expect.objectContaining({
|
functionResponse: {
|
||||||
functionResponse: expect.objectContaining({
|
id: 'multi1',
|
||||||
response: { output: 'Output 1' },
|
name: 'tool1',
|
||||||
}),
|
response: { output: 'Output 1' },
|
||||||
}),
|
},
|
||||||
]),
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
expect(call2Result).toMatchObject({
|
expect(call2Result).toMatchObject({
|
||||||
|
@ -931,13 +811,13 @@ describe('useReactToolScheduler', () => {
|
||||||
request: requests[1],
|
request: requests[1],
|
||||||
response: expect.objectContaining({
|
response: expect.objectContaining({
|
||||||
resultDisplay: 'Display 2',
|
resultDisplay: 'Display 2',
|
||||||
responseParts: expect.arrayContaining([
|
responseParts: {
|
||||||
expect.objectContaining({
|
functionResponse: {
|
||||||
functionResponse: expect.objectContaining({
|
id: 'multi2',
|
||||||
response: { output: 'Output 2' },
|
name: 'tool2',
|
||||||
}),
|
response: { output: 'Output 2' },
|
||||||
}),
|
},
|
||||||
]),
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
expect(result.current[0]).toEqual([]);
|
expect(result.current[0]).toEqual([]);
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { convertToFunctionResponse } from './coreToolScheduler.js';
|
||||||
|
import { Part, PartListUnion } from '@google/genai';
|
||||||
|
|
||||||
|
describe('convertToFunctionResponse', () => {
|
||||||
|
const toolName = 'testTool';
|
||||||
|
const callId = 'call1';
|
||||||
|
|
||||||
|
it('should handle simple string llmContent', () => {
|
||||||
|
const llmContent = 'Simple text output';
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual({
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: { output: 'Simple text output' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent as a single Part with text', () => {
|
||||||
|
const llmContent: Part = { text: 'Text from Part object' };
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual({
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: { output: 'Text from Part object' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent as a PartListUnion array with a single text Part', () => {
|
||||||
|
const llmContent: PartListUnion = [{ text: 'Text from array' }];
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual({
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: { output: 'Text from array' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent with inlineData', () => {
|
||||||
|
const llmContent: Part = {
|
||||||
|
inlineData: { mimeType: 'image/png', data: 'base64...' },
|
||||||
|
};
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: {
|
||||||
|
output: 'Binary content of type image/png was processed.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
llmContent,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent with fileData', () => {
|
||||||
|
const llmContent: Part = {
|
||||||
|
fileData: { mimeType: 'application/pdf', fileUri: 'gs://...' },
|
||||||
|
};
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: {
|
||||||
|
output: 'Binary content of type application/pdf was processed.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
llmContent,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent as an array of multiple Parts (text and inlineData)', () => {
|
||||||
|
const llmContent: PartListUnion = [
|
||||||
|
{ text: 'Some textual description' },
|
||||||
|
{ inlineData: { mimeType: 'image/jpeg', data: 'base64data...' } },
|
||||||
|
{ text: 'Another text part' },
|
||||||
|
];
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: { output: 'Tool execution succeeded.' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...llmContent,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent as an array with a single inlineData Part', () => {
|
||||||
|
const llmContent: PartListUnion = [
|
||||||
|
{ inlineData: { mimeType: 'image/gif', data: 'gifdata...' } },
|
||||||
|
];
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: {
|
||||||
|
output: 'Binary content of type image/gif was processed.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...llmContent,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent as a generic Part (not text, inlineData, or fileData)', () => {
|
||||||
|
const llmContent: Part = { functionCall: { name: 'test', args: {} } };
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual({
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: { output: 'Tool execution succeeded.' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string llmContent', () => {
|
||||||
|
const llmContent = '';
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual({
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: { output: '' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent as an empty array', () => {
|
||||||
|
const llmContent: PartListUnion = [];
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: { output: 'Tool execution succeeded.' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle llmContent as a Part with undefined inlineData/fileData/text', () => {
|
||||||
|
const llmContent: Part = {}; // An empty part object
|
||||||
|
const result = convertToFunctionResponse(toolName, callId, llmContent);
|
||||||
|
expect(result).toEqual({
|
||||||
|
functionResponse: {
|
||||||
|
name: toolName,
|
||||||
|
id: callId,
|
||||||
|
response: { output: 'Tool execution succeeded.' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -14,7 +14,8 @@ import {
|
||||||
ToolRegistry,
|
ToolRegistry,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
} from '../index.js';
|
} from '../index.js';
|
||||||
import { Part, PartUnion, PartListUnion } from '@google/genai';
|
import { Part, PartListUnion } from '@google/genai';
|
||||||
|
import { getResponseTextFromParts } from '../utils/generateContentResponseUtilities.js';
|
||||||
|
|
||||||
export type ValidatingToolCall = {
|
export type ValidatingToolCall = {
|
||||||
status: 'validating';
|
status: 'validating';
|
||||||
|
@ -96,51 +97,79 @@ export type ToolCallsUpdateHandler = (toolCalls: ToolCall[]) => void;
|
||||||
/**
|
/**
|
||||||
* Formats tool output for a Gemini FunctionResponse.
|
* Formats tool output for a Gemini FunctionResponse.
|
||||||
*/
|
*/
|
||||||
export function formatLlmContentForFunctionResponse(
|
function createFunctionResponsePart(
|
||||||
llmContent: PartListUnion,
|
callId: string,
|
||||||
): {
|
toolName: string,
|
||||||
functionResponseJson: Record<string, string>;
|
output: string,
|
||||||
additionalParts: PartUnion[];
|
): Part {
|
||||||
} {
|
return {
|
||||||
const additionalParts: PartUnion[] = [];
|
functionResponse: {
|
||||||
let functionResponseJson: Record<string, string>;
|
id: callId,
|
||||||
|
name: toolName,
|
||||||
|
response: { output },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertToFunctionResponse(
|
||||||
|
toolName: string,
|
||||||
|
callId: string,
|
||||||
|
llmContent: PartListUnion,
|
||||||
|
): PartListUnion {
|
||||||
const contentToProcess =
|
const contentToProcess =
|
||||||
Array.isArray(llmContent) && llmContent.length === 1
|
Array.isArray(llmContent) && llmContent.length === 1
|
||||||
? llmContent[0]
|
? llmContent[0]
|
||||||
: llmContent;
|
: llmContent;
|
||||||
|
|
||||||
if (typeof contentToProcess === 'string') {
|
if (typeof contentToProcess === 'string') {
|
||||||
functionResponseJson = { output: contentToProcess };
|
return createFunctionResponsePart(callId, toolName, contentToProcess);
|
||||||
} else if (Array.isArray(contentToProcess)) {
|
}
|
||||||
functionResponseJson = {
|
|
||||||
status: 'Tool execution succeeded.',
|
if (Array.isArray(contentToProcess)) {
|
||||||
};
|
const functionResponse = createFunctionResponsePart(
|
||||||
additionalParts.push(...contentToProcess);
|
callId,
|
||||||
} else if (contentToProcess.inlineData || contentToProcess.fileData) {
|
toolName,
|
||||||
|
'Tool execution succeeded.',
|
||||||
|
);
|
||||||
|
return [functionResponse, ...contentToProcess];
|
||||||
|
}
|
||||||
|
|
||||||
|
// After this point, contentToProcess is a single Part object.
|
||||||
|
if (contentToProcess.functionResponse) {
|
||||||
|
if (contentToProcess.functionResponse.response?.content) {
|
||||||
|
const stringifiedOutput =
|
||||||
|
getResponseTextFromParts(
|
||||||
|
contentToProcess.functionResponse.response.content as Part[],
|
||||||
|
) || '';
|
||||||
|
return createFunctionResponsePart(callId, toolName, stringifiedOutput);
|
||||||
|
}
|
||||||
|
// It's a functionResponse that we should pass through as is.
|
||||||
|
return contentToProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentToProcess.inlineData || contentToProcess.fileData) {
|
||||||
const mimeType =
|
const mimeType =
|
||||||
contentToProcess.inlineData?.mimeType ||
|
contentToProcess.inlineData?.mimeType ||
|
||||||
contentToProcess.fileData?.mimeType ||
|
contentToProcess.fileData?.mimeType ||
|
||||||
'unknown';
|
'unknown';
|
||||||
functionResponseJson = {
|
const functionResponse = createFunctionResponsePart(
|
||||||
status: `Binary content of type ${mimeType} was processed.`,
|
callId,
|
||||||
};
|
toolName,
|
||||||
additionalParts.push(contentToProcess);
|
`Binary content of type ${mimeType} was processed.`,
|
||||||
} else if (contentToProcess.text !== undefined) {
|
|
||||||
functionResponseJson = { output: contentToProcess.text };
|
|
||||||
} else if (contentToProcess.functionResponse) {
|
|
||||||
functionResponseJson = JSON.parse(
|
|
||||||
JSON.stringify(contentToProcess.functionResponse),
|
|
||||||
);
|
);
|
||||||
} else {
|
return [functionResponse, contentToProcess];
|
||||||
functionResponseJson = { status: 'Tool execution succeeded.' };
|
|
||||||
additionalParts.push(contentToProcess);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (contentToProcess.text !== undefined) {
|
||||||
functionResponseJson,
|
return createFunctionResponsePart(callId, toolName, contentToProcess.text);
|
||||||
additionalParts,
|
}
|
||||||
};
|
|
||||||
|
// Default case for other kinds of parts.
|
||||||
|
return createFunctionResponsePart(
|
||||||
|
callId,
|
||||||
|
toolName,
|
||||||
|
'Tool execution succeeded.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createErrorResponse = (
|
const createErrorResponse = (
|
||||||
|
@ -452,20 +481,15 @@ export class CoreToolScheduler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { functionResponseJson, additionalParts } =
|
const response = convertToFunctionResponse(
|
||||||
formatLlmContentForFunctionResponse(toolResult.llmContent);
|
toolName,
|
||||||
|
callId,
|
||||||
const functionResponsePart: Part = {
|
toolResult.llmContent,
|
||||||
functionResponse: {
|
);
|
||||||
name: toolName,
|
|
||||||
id: callId,
|
|
||||||
response: functionResponseJson,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const successResponse: ToolCallResponseInfo = {
|
const successResponse: ToolCallResponseInfo = {
|
||||||
callId,
|
callId,
|
||||||
responseParts: [functionResponsePart, ...additionalParts],
|
responseParts: response,
|
||||||
resultDisplay: toolResult.returnDisplay,
|
resultDisplay: toolResult.returnDisplay,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -81,15 +81,13 @@ describe('executeToolCall', () => {
|
||||||
expect(response.callId).toBe('call1');
|
expect(response.callId).toBe('call1');
|
||||||
expect(response.error).toBeUndefined();
|
expect(response.error).toBeUndefined();
|
||||||
expect(response.resultDisplay).toBe('Success!');
|
expect(response.resultDisplay).toBe('Success!');
|
||||||
expect(response.responseParts).toEqual([
|
expect(response.responseParts).toEqual({
|
||||||
{
|
functionResponse: {
|
||||||
functionResponse: {
|
name: 'testTool',
|
||||||
name: 'testTool',
|
id: 'call1',
|
||||||
id: 'call1',
|
response: { output: 'Tool executed successfully' },
|
||||||
response: { output: 'Tool executed successfully' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an error if tool is not found', async () => {
|
it('should return an error if tool is not found', async () => {
|
||||||
|
@ -225,7 +223,7 @@ describe('executeToolCall', () => {
|
||||||
name: 'testTool',
|
name: 'testTool',
|
||||||
id: 'call5',
|
id: 'call5',
|
||||||
response: {
|
response: {
|
||||||
status: 'Binary content of type image/png was processed.',
|
output: 'Binary content of type image/png was processed.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,14 +4,13 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Part } from '@google/genai';
|
|
||||||
import {
|
import {
|
||||||
ToolCallRequestInfo,
|
ToolCallRequestInfo,
|
||||||
ToolCallResponseInfo,
|
ToolCallResponseInfo,
|
||||||
ToolRegistry,
|
ToolRegistry,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
} from '../index.js';
|
} from '../index.js';
|
||||||
import { formatLlmContentForFunctionResponse } from './coreToolScheduler.js';
|
import { convertToFunctionResponse } from './coreToolScheduler.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a single tool call non-interactively.
|
* Executes a single tool call non-interactively.
|
||||||
|
@ -54,20 +53,15 @@ export async function executeToolCall(
|
||||||
// No live output callback for non-interactive mode
|
// No live output callback for non-interactive mode
|
||||||
);
|
);
|
||||||
|
|
||||||
const { functionResponseJson, additionalParts } =
|
const response = convertToFunctionResponse(
|
||||||
formatLlmContentForFunctionResponse(toolResult.llmContent);
|
toolCallRequest.name,
|
||||||
|
toolCallRequest.callId,
|
||||||
const functionResponsePart: Part = {
|
toolResult.llmContent,
|
||||||
functionResponse: {
|
);
|
||||||
name: toolCallRequest.name,
|
|
||||||
id: toolCallRequest.callId,
|
|
||||||
response: functionResponseJson,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
callId: toolCallRequest.callId,
|
callId: toolCallRequest.callId,
|
||||||
responseParts: [functionResponsePart, ...additionalParts],
|
responseParts: response,
|
||||||
resultDisplay: toolResult.returnDisplay,
|
resultDisplay: toolResult.returnDisplay,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -138,12 +138,7 @@ describe('DiscoveredMCPTool', () => {
|
||||||
const stringifiedResponseContent = JSON.stringify(
|
const stringifiedResponseContent = JSON.stringify(
|
||||||
mockToolSuccessResultObject,
|
mockToolSuccessResultObject,
|
||||||
);
|
);
|
||||||
// getStringifiedResultForDisplay joins text parts, then wraps the array of processed parts in JSON
|
expect(toolResult.returnDisplay).toBe(stringifiedResponseContent);
|
||||||
const expectedDisplayOutput =
|
|
||||||
'```json\n' +
|
|
||||||
JSON.stringify([stringifiedResponseContent], null, 2) +
|
|
||||||
'\n```';
|
|
||||||
expect(toolResult.returnDisplay).toBe(expectedDisplayOutput);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty result from getStringifiedResultForDisplay', async () => {
|
it('should handle empty result from getStringifiedResultForDisplay', async () => {
|
||||||
|
|
|
@ -149,6 +149,13 @@ function getStringifiedResultForDisplay(result: Part[]) {
|
||||||
return part; // Fallback for unexpected structure or non-FunctionResponsePart
|
return part; // Fallback for unexpected structure or non-FunctionResponsePart
|
||||||
};
|
};
|
||||||
|
|
||||||
const processedResults = result.map(processFunctionResponse);
|
const processedResults =
|
||||||
|
result.length === 1
|
||||||
|
? processFunctionResponse(result[0])
|
||||||
|
: result.map(processFunctionResponse);
|
||||||
|
if (typeof processedResults === 'string') {
|
||||||
|
return processedResults;
|
||||||
|
}
|
||||||
|
|
||||||
return '```json\n' + JSON.stringify(processedResults, null, 2) + '\n```';
|
return '```json\n' + JSON.stringify(processedResults, null, 2) + '\n```';
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GenerateContentResponse } from '@google/genai';
|
import { GenerateContentResponse, Part } from '@google/genai';
|
||||||
|
|
||||||
export function getResponseText(
|
export function getResponseText(
|
||||||
response: GenerateContentResponse,
|
response: GenerateContentResponse,
|
||||||
|
@ -15,3 +15,7 @@ export function getResponseText(
|
||||||
.join('') || undefined
|
.join('') || undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getResponseTextFromParts(parts: Part[]): string | undefined {
|
||||||
|
return parts?.map((part) => part.text).join('') || undefined;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue