@@ -395,11 +395,18 @@ private async Task ProcessFunctionsAsync(ChatCompletionState state, Cancellation
395395
396396 // We must send back a response for every tool call, regardless of whether we successfully executed it or not.
397397 // If we successfully execute it, we'll add the result. If we don't, we'll add an error.
398+ // Collect all tool responses before adding to chat history
399+ var toolResponses = new List < GeminiChatMessageContent > ( ) ;
400+
398401 foreach ( var toolCall in state . LastMessage ! . ToolCalls ! )
399402 {
400- await this . ProcessSingleToolCallAsync ( state , toolCall , cancellationToken ) . ConfigureAwait ( false ) ;
403+ var toolResponse = await this . ProcessSingleToolCallAndReturnResponseAsync ( state , toolCall , cancellationToken ) . ConfigureAwait ( false ) ;
404+ toolResponses . Add ( toolResponse ) ;
401405 }
402406
407+ // Add all tool responses as a single batched message
408+ this . AddBatchedToolResponseMessage ( state . ChatHistory , state . GeminiRequest , toolResponses ) ;
409+
403410 // Clear the tools. If we end up wanting to use tools, we'll reset it to the desired value.
404411 state . GeminiRequest . Tools = null ;
405412
@@ -431,6 +438,46 @@ private async Task ProcessFunctionsAsync(ChatCompletionState state, Cancellation
431438 }
432439 }
433440
441+ private void AddBatchedToolResponseMessage (
442+ ChatHistory chat ,
443+ GeminiRequest request ,
444+ List < GeminiChatMessageContent > toolResponses )
445+ {
446+ if ( toolResponses . Count == 0 )
447+ {
448+ return ;
449+ }
450+
451+ // Extract all tool results and combine content
452+ var allToolResults = toolResponses
453+ . Where ( tr => tr . CalledToolResults != null )
454+ . SelectMany ( tr => tr . CalledToolResults ! )
455+ . ToList ( ) ;
456+
457+ // Combine tool response content as a JSON array for better structure
458+ var combinedContentList = toolResponses
459+ . Select ( tr => tr . Content )
460+ . Where ( c => ! string . IsNullOrEmpty ( c ) )
461+ . ToList ( ) ;
462+
463+ var combinedContent = combinedContentList . Count switch
464+ {
465+ 0 => string . Empty ,
466+ 1 => combinedContentList [ 0 ] ,
467+ _ => JsonSerializer . Serialize ( combinedContentList )
468+ } ;
469+
470+ // Create a single message with all function response parts using the new constructor
471+ var batchedMessage = new GeminiChatMessageContent (
472+ AuthorRole . Tool ,
473+ combinedContent ,
474+ this . _modelId ,
475+ calledToolResults : allToolResults ) ;
476+
477+ chat . Add ( batchedMessage ) ;
478+ request . AddChatMessage ( batchedMessage ) ;
479+ }
480+
434481 private async Task ProcessSingleToolCallAsync ( ChatCompletionState state , GeminiFunctionToolCall toolCall , CancellationToken cancellationToken )
435482 {
436483 // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked,
@@ -480,6 +527,65 @@ private async Task ProcessSingleToolCallAsync(ChatCompletionState state, GeminiF
480527 functionResponse : functionResult , errorMessage : null ) ;
481528 }
482529
530+ private async Task < GeminiChatMessageContent > ProcessSingleToolCallAndReturnResponseAsync ( ChatCompletionState state , GeminiFunctionToolCall toolCall , CancellationToken cancellationToken )
531+ {
532+ // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked,
533+ // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able
534+ // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check.
535+ if ( state . ExecutionSettings . ToolCallBehavior ? . AllowAnyRequestedKernelFunction is not true &&
536+ ! IsRequestableTool ( state . GeminiRequest . Tools ! [ 0 ] . Functions , toolCall ) )
537+ {
538+ return this . CreateToolResponseMessage ( toolCall , functionResponse : null , "Error: Function call request for a function that wasn't defined." ) ;
539+ }
540+
541+ // Ensure the provided function exists for calling
542+ if ( ! state . Kernel ! . Plugins . TryGetFunctionAndArguments ( toolCall , out KernelFunction ? function , out KernelArguments ? functionArgs ) )
543+ {
544+ return this . CreateToolResponseMessage ( toolCall , functionResponse : null , "Error: Requested function could not be found." ) ;
545+ }
546+
547+ // Now, invoke the function, and create the resulting tool call message.
548+ s_inflightAutoInvokes . Value ++ ;
549+ FunctionResult ? functionResult ;
550+ try
551+ {
552+ // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any
553+ // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here,
554+ // as the called function could in turn telling the model about itself as a possible candidate for invocation.
555+ functionResult = await function . InvokeAsync ( state . Kernel , functionArgs , cancellationToken : cancellationToken )
556+ . ConfigureAwait ( false ) ;
557+ }
558+ #pragma warning disable CA1031 // Do not catch general exception types
559+ catch ( Exception e )
560+ #pragma warning restore CA1031
561+ {
562+ return this . CreateToolResponseMessage ( toolCall , functionResponse : null , $ "Error: Exception while invoking function. { e . Message } ") ;
563+ }
564+ finally
565+ {
566+ s_inflightAutoInvokes . Value -- ;
567+ }
568+
569+ return this . CreateToolResponseMessage ( toolCall , functionResponse : functionResult , errorMessage : null ) ;
570+ }
571+
572+ private GeminiChatMessageContent CreateToolResponseMessage (
573+ GeminiFunctionToolCall tool ,
574+ FunctionResult ? functionResponse ,
575+ string ? errorMessage )
576+ {
577+ if ( errorMessage is not null && this . Logger . IsEnabled ( LogLevel . Debug ) )
578+ {
579+ this . Logger . LogDebug ( "Failed to handle tool request ({ToolName}). {Error}" , tool . FullyQualifiedName , errorMessage ) ;
580+ }
581+
582+ return new GeminiChatMessageContent ( AuthorRole . Tool ,
583+ content : errorMessage ?? string . Empty ,
584+ modelId : this . _modelId ,
585+ calledToolResult : functionResponse is not null ? new GeminiFunctionToolResult ( tool , functionResponse ) : null ,
586+ metadata : null ) ;
587+ }
588+
483589 private async Task < GeminiResponse > SendRequestAndReturnValidGeminiResponseAsync (
484590 Uri endpoint ,
485591 GeminiRequest geminiRequest ,
@@ -604,7 +710,7 @@ private void LogUsage(List<GeminiChatMessageContent> chatMessageContents)
604710
605711 private List < GeminiChatMessageContent > GetChatMessageContentsFromResponse ( GeminiResponse geminiResponse )
606712 => geminiResponse . Candidates == null ?
607- [ new GeminiChatMessageContent ( role : AuthorRole . Assistant , content : string . Empty , modelId : this . _modelId ) ]
713+ [ new GeminiChatMessageContent ( role : AuthorRole . Assistant , content : string . Empty , modelId : this . _modelId , functionsToolCalls : null ) ]
608714 : geminiResponse . Candidates . Select ( candidate => this . GetChatMessageContentFromCandidate ( geminiResponse , candidate ) ) . ToList ( ) ;
609715
610716 private GeminiChatMessageContent GetChatMessageContentFromCandidate ( GeminiResponse geminiResponse , GeminiResponseCandidate candidate )
0 commit comments