88 type GoogleGenAIOptions ,
99 type Schema ,
1010 type Part ,
11+ type FunctionCall ,
12+ createPartFromFunctionResponse ,
1113} from "@google/genai" ;
1214import type {
1315 BaseMessage ,
@@ -19,15 +21,17 @@ import type {
1921 ProviderTextStreamResponse ,
2022 StepWithContent ,
2123 UsageInfo ,
24+ BaseTool ,
2225} from "@voltagent/core" ;
2326import type { z } from "zod" ;
2427import type {
2528 GoogleGenerateContentStreamResult ,
26- GoogleGenerateTextOptions ,
2729 GoogleProviderRuntimeOptions ,
2830 GoogleStreamTextOptions ,
31+ GoogleGenerateTextOptions ,
2932} from "./types" ;
3033import { isZodObject , responseSchemaFromZodType } from "./utils/schema_helper" ;
34+ import { prepareToolsForGoogleSDK , executeFunctionCalls } from "./utils/function-calling" ;
3135
3236type 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 }
0 commit comments