Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export {
GoogleMapsGroundingTool,
} from './tools/google_maps_grounding_tool.js';
export {GOOGLE_SEARCH, GoogleSearchTool} from './tools/google_search_tool.js';
export type {GoogleSearchToolParams} from './tools/google_search_tool.js';
export {
LOAD_ARTIFACTS,
LoadArtifactsTool,
Expand Down
33 changes: 31 additions & 2 deletions core/src/tools/google_search_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ import {isGemini1Model, isGeminiModel} from '../utils/model_name.js';

import {BaseTool, ToolProcessLlmRequest} from './base_tool.js';

/** Parameters for constructing a {@link GoogleSearchTool}. */
export interface GoogleSearchToolParams {
/**
* Whether the tool may be used alongside other tools on a Gemini 1.x model.
*
* Gemini 1.x rejects Google Search combined with other tools, so by default
* `processLlmRequest` throws in that case. Set to `true` to opt out of that
* check.
*/
bypassMultiToolsLimit?: boolean;

/**
* Model name to process the request as, instead of the model on the
* incoming request. When set, it replaces `llmRequest.model` before the
* model family is inspected.
*/
model?: string;
}

/**
* A built-in tool that is automatically invoked by Gemini 2 models to retrieve
* search results from Google Search.
Expand All @@ -17,8 +36,14 @@ import {BaseTool, ToolProcessLlmRequest} from './base_tool.js';
* perform local code execution.
*/
export class GoogleSearchTool extends BaseTool {
constructor() {
readonly bypassMultiToolsLimit: boolean;
readonly model?: string;

constructor(params: GoogleSearchToolParams = {}) {
super({name: 'google_search', description: 'Google Search Tool'});

this.bypassMultiToolsLimit = params.bypassMultiToolsLimit ?? false;
this.model = params.model;
}

runAsync(): Promise<unknown> {
Expand All @@ -30,6 +55,10 @@ export class GoogleSearchTool extends BaseTool {
override async processLlmRequest({
llmRequest,
}: ToolProcessLlmRequest): Promise<void> {
if (this.model !== undefined) {
llmRequest.model = this.model;
}

if (!llmRequest.model) {
return;
}
Expand All @@ -38,7 +67,7 @@ export class GoogleSearchTool extends BaseTool {
llmRequest.config.tools = llmRequest.config.tools || [];

if (isGemini1Model(llmRequest.model)) {
if (llmRequest.config.tools.length > 0) {
if (llmRequest.config.tools.length > 0 && !this.bypassMultiToolsLimit) {
throw new Error(
'Google search tool can not be used with other tools in Gemini 1.x.',
);
Expand Down
124 changes: 122 additions & 2 deletions core/test/tools/google_search_tool_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {GOOGLE_SEARCH, GoogleSearchTool, LlmRequest} from '@google/adk';
import {
Context,
createSession,
GOOGLE_SEARCH,
GoogleSearchTool,
InvocationContext,
LlmAgent,
LlmRequest,
PluginManager,
} from '@google/adk';
import {Tool} from '@google/genai';
import {describe, expect, it} from 'vitest';

function makeRequest(model?: string, tools = []): LlmRequest {
function makeRequest(model?: string, tools: Tool[] = []): LlmRequest {
return {
model,
config: {tools},
Expand All @@ -17,6 +27,18 @@ function makeRequest(model?: string, tools = []): LlmRequest {
} as unknown as LlmRequest;
}

/** Builds a real `Context` backed by real ADK plumbing, with no stubs. */
function makeToolContext(): Context {
return new Context({
invocationContext: new InvocationContext({
invocationId: 'test-invocation',
agent: new LlmAgent({name: 'test_agent'}),
session: createSession({id: 'test-session', appName: 'test-app'}),
pluginManager: new PluginManager([]),
}),
});
}

describe('GoogleSearchTool', () => {
describe('processLlmRequest', () => {
it('returns early when model is not set', async () => {
Expand Down Expand Up @@ -91,9 +113,107 @@ describe('GoogleSearchTool', () => {

expect(req.config!.tools).toEqual([{googleSearch: {}}]);
});

it('skips the Gemini 1.x multi-tool check when bypassMultiToolsLimit is true', async () => {
const tool = new GoogleSearchTool({bypassMultiToolsLimit: true});
const req = makeRequest('gemini-1.5-pro', [{functionDeclarations: []}]);
await tool.processLlmRequest({
llmRequest: req,
toolContext: makeToolContext(),
});

expect(req.config!.tools).toEqual([
{functionDeclarations: []},
{googleSearchRetrieval: {}},
]);
});

it('still throws on a Gemini 1.x multi-tool request when bypassMultiToolsLimit is false', async () => {
const tool = new GoogleSearchTool({bypassMultiToolsLimit: false});
const req = makeRequest('gemini-1.5-pro', [{functionDeclarations: []}]);
await expect(
tool.processLlmRequest({
llmRequest: req,
toolContext: makeToolContext(),
}),
).rejects.toThrow(
'Google search tool can not be used with other tools in Gemini 1.x.',
);
});

it('applies a Gemini 1.x model override to a Gemini 2+ request', async () => {
const tool = new GoogleSearchTool({model: 'gemini-1.5-pro'});
const req = makeRequest('gemini-2.0-flash');
await tool.processLlmRequest({
llmRequest: req,
toolContext: makeToolContext(),
});

expect(req.config!.tools).toEqual([{googleSearchRetrieval: {}}]);
expect(req.model).toBe('gemini-1.5-pro');
});

it('applies a Gemini 2+ model override to a Gemini 1.x request', async () => {
const tool = new GoogleSearchTool({model: 'gemini-2.0-flash'});
const req = makeRequest('gemini-1.5-pro');
await tool.processLlmRequest({
llmRequest: req,
toolContext: makeToolContext(),
});

expect(req.config!.tools).toEqual([{googleSearch: {}}]);
expect(req.model).toBe('gemini-2.0-flash');
});

it('applies the model override when the request carries no model', async () => {
const tool = new GoogleSearchTool({model: 'gemini-2.0-flash'});
const req = makeRequest(undefined);
await tool.processLlmRequest({
llmRequest: req,
toolContext: makeToolContext(),
});

expect(req.config!.tools).toEqual([{googleSearch: {}}]);
expect(req.model).toBe('gemini-2.0-flash');
});

it('leaves the request model untouched when no override is set', async () => {
const tool = new GoogleSearchTool();
const req = makeRequest('gemini-2.0-flash');
await tool.processLlmRequest({
llmRequest: req,
toolContext: makeToolContext(),
});

expect(req.model).toBe('gemini-2.0-flash');
expect(req.config!.tools).toEqual([{googleSearch: {}}]);
});

it('leaves Gemini 2+ behaviour unchanged when bypassMultiToolsLimit is true', async () => {
const tool = new GoogleSearchTool({bypassMultiToolsLimit: true});
const req = makeRequest('gemini-2.0-flash', [{functionDeclarations: []}]);
await tool.processLlmRequest({
llmRequest: req,
toolContext: makeToolContext(),
});

expect(req.config!.tools).toEqual([
{functionDeclarations: []},
{googleSearch: {}},
]);
});
});

it('has a global instance GOOGLE_SEARCH', () => {
expect(GOOGLE_SEARCH).toBeInstanceOf(GoogleSearchTool);
});

it('defaults to no bypass and no model override', () => {
const tool = new GoogleSearchTool();

expect(tool.bypassMultiToolsLimit).toBe(false);
expect(tool.model).toBeUndefined();
expect(GOOGLE_SEARCH.bypassMultiToolsLimit).toBe(false);
expect(GOOGLE_SEARCH.model).toBeUndefined();
});
});
Loading