Skip to content

Commit 75dfc56

Browse files
committed
feat(google-ai): add function calling support for Google SDK integration
1 parent f7de864 commit 75dfc56

4 files changed

Lines changed: 324 additions & 25 deletions

File tree

packages/google-ai/src/index.ts

Lines changed: 178 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
type GoogleGenAIOptions,
99
type Schema,
1010
type Part,
11+
type FunctionCall,
12+
createPartFromFunctionResponse,
1113
} from "@google/genai";
1214
import type {
1315
BaseMessage,
@@ -19,15 +21,17 @@ import type {
1921
ProviderTextStreamResponse,
2022
StepWithContent,
2123
UsageInfo,
24+
BaseTool,
2225
} from "@voltagent/core";
2326
import type { z } from "zod";
2427
import type {
2528
GoogleGenerateContentStreamResult,
26-
GoogleGenerateTextOptions,
2729
GoogleProviderRuntimeOptions,
2830
GoogleStreamTextOptions,
31+
GoogleGenerateTextOptions,
2932
} from "./types";
3033
import { isZodObject, responseSchemaFromZodType } from "./utils/schema_helper";
34+
import { prepareToolsForGoogleSDK, executeFunctionCalls } from "./utils/function-calling";
3135

3236
type StreamProcessingState = {
3337
accumulatedText: string;
@@ -67,6 +71,7 @@ export class GoogleGenAIProvider implements LLMProvider<string> {
6771
this._processStreamChunk = this._processStreamChunk.bind(this);
6872
this._finalizeStream = this._finalizeStream.bind(this);
6973
this.generateObject = this.generateObject.bind(this);
74+
this._handleFunctionCalling = this._handleFunctionCalling.bind(this);
7075
}
7176

7277
getModelIdentifier = (model: string): string => {
@@ -159,23 +164,59 @@ export class GoogleGenAIProvider implements LLMProvider<string> {
159164
return { role, parts: [{ text: "" }] };
160165
};
161166

162-
private _createStepFromChunk(
163-
response: GenerateContentResponse,
164-
role: MessageRole = "assistant",
165-
usage?: UsageInfo,
166-
): StepWithContent | null {
167-
const text = response.text;
168-
if (text !== undefined && text !== "") {
167+
private _createStepFromChunk = (chunk: {
168+
type: string;
169+
[key: string]: any;
170+
}): StepWithContent | null => {
171+
if (chunk.type === "text" && chunk.text) {
169172
return {
170-
id: response.responseId || "",
173+
id: "",
171174
type: "text",
172-
content: text,
173-
role: role,
174-
usage: usage,
175+
content: chunk.text,
176+
role: "assistant" as MessageRole,
177+
usage: chunk.usage || undefined,
178+
};
179+
}
180+
181+
if (chunk.type === "tool-call" || chunk.type === "tool_call") {
182+
return {
183+
id: chunk.toolCallId,
184+
type: "tool_call",
185+
name: chunk.toolName,
186+
arguments: chunk.args,
187+
content: JSON.stringify([
188+
{
189+
type: "tool-call",
190+
toolCallId: chunk.toolCallId,
191+
toolName: chunk.toolName,
192+
args: chunk.args,
193+
},
194+
]),
195+
role: "assistant" as MessageRole,
196+
usage: chunk.usage || undefined,
175197
};
176198
}
199+
200+
if (chunk.type === "tool-result" || chunk.type === "tool_result") {
201+
return {
202+
id: chunk.toolCallId,
203+
type: "tool_result",
204+
name: chunk.toolName,
205+
result: chunk.result,
206+
content: JSON.stringify([
207+
{
208+
type: "tool-result",
209+
toolCallId: chunk.toolCallId,
210+
result: chunk.result,
211+
},
212+
]),
213+
role: "assistant" as MessageRole,
214+
usage: chunk.usage || undefined,
215+
};
216+
}
217+
177218
return null;
178-
}
219+
};
179220

180221
private _getUsageInfo(
181222
usageInfo: GenerateContentResponseUsageMetadata | undefined,
@@ -193,11 +234,98 @@ export class GoogleGenAIProvider implements LLMProvider<string> {
193234
return undefined;
194235
}
195236

237+
private async _handleFunctionCalling(
238+
initialResponse: GenerateContentResponse,
239+
functionCalls: FunctionCall[],
240+
availableTools: BaseTool[],
241+
originalContents: Content[],
242+
model: string,
243+
baseConfig: GenerateContentConfig,
244+
options: GoogleGenerateTextOptions,
245+
): Promise<GenerateContentResponse> {
246+
functionCalls.forEach((functionCall) => {
247+
if (!functionCall.id) {
248+
// should have a random id. For example: 'call_o0eSbOWhYH2mvL6ogbbXn5uH'
249+
functionCall.id = `call_${Math.random().toString(36).substring(2)}${Date.now().toString(36)}`;
250+
}
251+
});
252+
253+
if (options.onStepFinish) {
254+
// Set a tool state when it is called
255+
for (const functionCall of functionCalls) {
256+
const step = this._createStepFromChunk({
257+
type: "tool-call",
258+
toolCallId: functionCall.id,
259+
toolName: functionCall.name,
260+
args: functionCall.args,
261+
});
262+
if (step) await options.onStepFinish(step);
263+
}
264+
}
265+
266+
const functionResponses = await executeFunctionCalls(functionCalls, availableTools);
267+
268+
if (options.onStepFinish) {
269+
// Set a tool state with the result
270+
for (const funcResponse of functionResponses) {
271+
const step = this._createStepFromChunk({
272+
type: "tool-result",
273+
toolCallId: funcResponse.id,
274+
toolName: funcResponse.name,
275+
result: funcResponse.response?.output,
276+
usage: undefined,
277+
});
278+
if (step) await options.onStepFinish(step);
279+
}
280+
}
281+
282+
const functionResponseParts: Part[] = functionResponses
283+
.map((funcResponse) => {
284+
if (!funcResponse.id || !funcResponse.name || !funcResponse.response) {
285+
console.debug(
286+
"[GoogleGenAIProvider] Invalid FunctionResponse format, skipping:",
287+
funcResponse,
288+
);
289+
return null;
290+
}
291+
return createPartFromFunctionResponse(
292+
funcResponse.id,
293+
funcResponse.name,
294+
funcResponse.response,
295+
);
296+
})
297+
.filter((part): part is Part => part !== null);
298+
299+
if (functionResponseParts.length === 0) {
300+
console.debug(
301+
"[GoogleGenAIProvider] No valid function response parts generated. Returning initial response.",
302+
);
303+
return initialResponse;
304+
}
305+
306+
const updatedContents = [...originalContents];
307+
const modelTurnContent = initialResponse.candidates?.[0]?.content;
308+
if (modelTurnContent) {
309+
updatedContents.push(modelTurnContent);
310+
}
311+
updatedContents.push({ role: "user", parts: functionResponseParts });
312+
// Remove tools for the second call(final response from the model)
313+
const { tools, toolConfig, ...secondCallConfig } = baseConfig;
314+
const generationParams: GenerateContentParameters = {
315+
contents: updatedContents,
316+
model: model,
317+
...(Object.keys(secondCallConfig).length > 0 ? { config: secondCallConfig } : {}),
318+
};
319+
320+
const finalResult = await this.ai.models.generateContent(generationParams);
321+
return finalResult;
322+
}
323+
196324
generateText = async (
197325
options: GoogleGenerateTextOptions,
198326
): Promise<ProviderTextResponse<GenerateContentResponse>> => {
199327
const model = options.model;
200-
const contents = options.messages.map(this.toMessage);
328+
const currentContents = options.messages.map(this.toMessage);
201329
const providerOptions: GoogleProviderRuntimeOptions = options.provider || {};
202330

203331
const config: GenerateContentConfig = {
@@ -210,27 +338,51 @@ export class GoogleGenAIProvider implements LLMProvider<string> {
210338
...(providerOptions.extraOptions && providerOptions.extraOptions),
211339
};
212340

341+
// Add tools configuration if tools are provided for the inital model call.
342+
const availableTools: BaseTool[] = options.tools || [];
343+
if (availableTools.length > 0) {
344+
const { tools, toolConfig } = prepareToolsForGoogleSDK(availableTools, this.isVertexAI);
345+
Object.assign(config, { tools: [tools], toolConfig });
346+
}
347+
348+
// Remove undefined keys from config
213349
Object.keys(config).forEach(
214350
(key) => (config as any)[key] === undefined && delete (config as any)[key],
215351
);
216352

353+
// Initial Generation Call
217354
const generationParams: GenerateContentParameters = {
218-
contents: contents,
355+
contents: currentContents,
219356
model: model,
220357
...(Object.keys(config).length > 0 ? { config: config } : {}),
221358
};
222359

223-
const result = await this.ai.models.generateContent(generationParams);
224-
225-
const response = result;
360+
let response = await this.ai.models.generateContent(generationParams);
361+
const functionCalls = response.functionCalls;
362+
if (functionCalls && functionCalls.length > 0) {
363+
// If the model returns function calls, handle and execute them.
364+
response = await this._handleFunctionCalling(
365+
response,
366+
functionCalls,
367+
availableTools,
368+
currentContents,
369+
model,
370+
config,
371+
options,
372+
);
373+
}
226374

227375
const responseText = response.text;
228376
const usageInfo = response?.usageMetadata;
229377
const finishReason = response.candidates?.[0]?.finishReason?.toString();
230378
const finalUsage = this._getUsageInfo(usageInfo);
231379

232380
if (options.onStepFinish) {
233-
const step = this._createStepFromChunk(response, "assistant", finalUsage);
381+
const step = this._createStepFromChunk({
382+
type: "text",
383+
text: responseText,
384+
usage: finalUsage,
385+
});
234386
if (step) await options.onStepFinish(step);
235387
}
236388

@@ -266,7 +418,13 @@ export class GoogleGenAIProvider implements LLMProvider<string> {
266418
controller.enqueue(textChunk);
267419
state.accumulatedText += textChunk;
268420
if (options.onChunk) {
269-
const step = this._createStepFromChunk(chunkResponse, "assistant", undefined);
421+
const step = this._createStepFromChunk({
422+
id: chunkResponse.responseId || "",
423+
type: "text",
424+
text: chunkResponse.text,
425+
role: "assistant",
426+
usage: undefined,
427+
});
270428
if (step) await options.onChunk(step);
271429
}
272430
}

packages/google-ai/src/types.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { GenerateContentConfig, GenerateContentResponse } from "@google/genai";
1+
import type {
2+
GenerateContentConfig,
3+
GenerateContentResponse,
4+
ToolConfig,
5+
Tool,
6+
} from "@google/genai";
7+
import type { BaseMessage, BaseTool, StepWithContent } from "@voltagent/core";
28

39
// Define explicit runtime options to avoid deep generic instantiation
410
export interface GoogleProviderRuntimeOptions
@@ -10,15 +16,19 @@ export interface GoogleProviderRuntimeOptions
1016
[key: string]: any;
1117
}
1218

19+
// Tool configuration types based on Google's GenAI SDK
20+
export interface GoogleToolConfig extends ToolConfig {}
21+
export interface GoogleTool extends Tool {}
22+
1323
// Define concrete types instead of using Omit with generics since it was causing
1424
// "Type instantiation is excessively deep and possibly infinite".
1525
type BaseGoogleTextOptions = {
16-
messages: any[];
26+
messages: BaseMessage[];
1727
model: string;
1828
provider?: GoogleProviderRuntimeOptions;
19-
tools?: any[];
29+
tools?: BaseTool[];
2030
maxSteps?: number;
21-
onStepFinish?: (step: any) => void | Promise<void>;
31+
onStepFinish?: (step: StepWithContent) => void | Promise<void>;
2232
signal?: AbortSignal;
2333
};
2434

0 commit comments

Comments
 (0)