diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatClientFunctionCallingTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatClientFunctionCallingTests.cs
new file mode 100644
index 000000000000..49b96537bdf3
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatClientFunctionCallingTests.cs
@@ -0,0 +1,178 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.Google;
+using Microsoft.SemanticKernel.Connectors.Google.Core;
+using Xunit;
+
+namespace SemanticKernel.Connectors.Google.UnitTests.Core.Gemini.Clients;
+
+///
+/// Unit tests for IChatClient-based function calling with Gemini using FunctionChoiceBehavior.
+///
+public sealed class GeminiChatClientFunctionCallingTests : IDisposable
+{
+ private readonly HttpClient _httpClient;
+ private readonly string _responseContent;
+ private readonly string _responseContentWithFunction;
+ private readonly HttpMessageHandlerStub _messageHandlerStub;
+ private readonly GeminiFunction _timePluginDate, _timePluginNow;
+ private readonly Kernel _kernelWithFunctions;
+ private const string ChatTestDataFilePath = "./TestData/chat_one_response.json";
+ private const string ChatTestDataWithFunctionFilePath = "./TestData/chat_one_function_response.json";
+
+ public GeminiChatClientFunctionCallingTests()
+ {
+ this._responseContent = File.ReadAllText(ChatTestDataFilePath);
+ this._responseContentWithFunction = File.ReadAllText(ChatTestDataWithFunctionFilePath)
+ .Replace("%nameSeparator%", GeminiFunction.NameSeparator, StringComparison.Ordinal);
+ this._messageHandlerStub = new HttpMessageHandlerStub();
+ this._messageHandlerStub.ResponseToReturn.Content = new StringContent(
+ this._responseContent);
+
+ this._httpClient = new HttpClient(this._messageHandlerStub, false);
+
+ var kernelPlugin = KernelPluginFactory.CreateFromFunctions("TimePlugin", new[]
+ {
+ KernelFunctionFactory.CreateFromMethod((string? format = null)
+ => DateTime.Now.Date.ToString(format, CultureInfo.InvariantCulture), "Date", "TimePlugin.Date"),
+ KernelFunctionFactory.CreateFromMethod(()
+ => DateTime.Now.ToString("", CultureInfo.InvariantCulture), "Now", "TimePlugin.Now",
+ parameters: [new KernelParameterMetadata("param1") { ParameterType = typeof(string), Description = "desc", IsRequired = false }]),
+ });
+ IList functions = kernelPlugin.GetFunctionsMetadata();
+
+ this._timePluginDate = functions[0].ToGeminiFunction();
+ this._timePluginNow = functions[1].ToGeminiFunction();
+
+ this._kernelWithFunctions = new Kernel();
+ this._kernelWithFunctions.Plugins.Add(kernelPlugin);
+ }
+
+ [Fact]
+ public async Task ChatClientShouldConvertToIChatClientSuccessfullyAsync()
+ {
+ // Arrange
+ var chatCompletionService = this.CreateChatCompletionService();
+
+ // Act
+ var chatClient = chatCompletionService.AsChatClient();
+
+ // Assert - Verify conversion works
+ Assert.NotNull(chatClient);
+ Assert.IsAssignableFrom(chatClient);
+
+ // Verify we can make a basic call through IChatClient
+ var messages = new List
+ {
+ new(ChatRole.User, "What time is it?")
+ };
+
+ var response = await chatClient.GetResponseAsync(messages);
+
+ Assert.NotNull(response);
+ Assert.NotEmpty(response.Messages);
+ }
+
+ [Fact]
+ public async Task ChatClientShouldReceiveFunctionCallsInResponseAsync()
+ {
+ // Arrange
+ this._messageHandlerStub.ResponseToReturn.Content = new StringContent(this._responseContentWithFunction);
+ var chatCompletionService = this.CreateChatCompletionService();
+ var chatClient = chatCompletionService.AsChatClient();
+
+ var settings = new GeminiPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
+ };
+ var chatOptions = settings.ToChatOptions(this._kernelWithFunctions);
+
+ var messages = new List
+ {
+ new(ChatRole.User, "What time is it?")
+ };
+
+ // Act
+ var response = await chatClient.GetResponseAsync(messages, chatOptions);
+
+ // Assert - Verify that FunctionCallContent is returned in the response
+ Assert.NotNull(response);
+ var functionCalls = response.Messages
+ .SelectMany(m => m.Contents)
+ .OfType()
+ .ToList();
+
+ Assert.NotEmpty(functionCalls);
+ var functionCall = functionCalls.First();
+ Assert.Contains(this._timePluginNow.FunctionName, functionCall.Name, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task ChatClientShouldStreamResponsesAsync()
+ {
+ // Arrange
+ var chatCompletionService = this.CreateChatCompletionService();
+ var chatClient = chatCompletionService.AsChatClient();
+
+ var settings = new GeminiPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
+ };
+ var chatOptions = settings.ToChatOptions(this._kernelWithFunctions);
+
+ var messages = new List
+ {
+ new(ChatRole.User, "What time is it?")
+ };
+
+ // Act
+ var updates = new List();
+ await foreach (var update in chatClient.GetStreamingResponseAsync(messages, chatOptions))
+ {
+ updates.Add(update);
+ }
+
+ // Assert - Verify that streaming works and returns updates
+ Assert.NotEmpty(updates);
+ }
+
+ [Fact]
+ public async Task AsChatClientConvertsServiceToIChatClientAsync()
+ {
+ // Arrange
+ var chatCompletionService = this.CreateChatCompletionService();
+
+ // Act
+ var chatClient = chatCompletionService.AsChatClient();
+
+ // Assert
+ Assert.NotNull(chatClient);
+ Assert.IsAssignableFrom(chatClient);
+ }
+
+ private GoogleAIGeminiChatCompletionService CreateChatCompletionService(HttpClient? httpClient = null)
+ {
+ return new GoogleAIGeminiChatCompletionService(
+ modelId: "fake-model",
+ apiKey: "fake-key",
+ apiVersion: GoogleAIVersion.V1,
+ httpClient: httpClient ?? this._httpClient);
+ }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this._messageHandlerStub.Dispose();
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiToolCallBehaviorTests.cs
index 958f2ad27082..4833fac1a4cd 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiToolCallBehaviorTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiToolCallBehaviorTests.cs
@@ -194,6 +194,111 @@ public void KernelFunctionsCloneReturnsCorrectClone()
Assert.Equivalent(toolcallbehavior, clone, strict: true);
}
+ [Fact]
+ public void FunctionChoiceBehaviorAutoConvertsToAutoInvokeKernelFunctions()
+ {
+ // Arrange
+ var settings = new GeminiPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
+ };
+
+ // Act
+ var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
+
+ // Assert
+ Assert.NotNull(converted.ToolCallBehavior);
+ Assert.IsType(converted.ToolCallBehavior);
+ Assert.True(converted.ToolCallBehavior.MaximumAutoInvokeAttempts > 0);
+ }
+
+ [Fact]
+ public void FunctionChoiceBehaviorAutoWithNoAutoInvokeConvertsToEnableKernelFunctions()
+ {
+ // Arrange
+ var settings = new GeminiPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
+ };
+
+ // Act
+ var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
+
+ // Assert
+ Assert.NotNull(converted.ToolCallBehavior);
+ Assert.IsType(converted.ToolCallBehavior);
+ Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
+ }
+
+ [Fact]
+ public void FunctionChoiceBehaviorRequiredConvertsToAutoInvokeKernelFunctions()
+ {
+ // Arrange
+ var settings = new GeminiPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Required()
+ };
+
+ // Act
+ var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
+
+ // Assert
+ Assert.NotNull(converted.ToolCallBehavior);
+ Assert.IsType(converted.ToolCallBehavior);
+ Assert.True(converted.ToolCallBehavior.MaximumAutoInvokeAttempts > 0);
+ }
+
+ [Fact]
+ public void FunctionChoiceBehaviorNoneConvertsToEnableKernelFunctions()
+ {
+ // Arrange
+ var settings = new GeminiPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.None()
+ };
+
+ // Act
+ var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
+
+ // Assert
+ Assert.NotNull(converted.ToolCallBehavior);
+ Assert.IsType(converted.ToolCallBehavior);
+ // None behavior doesn't auto-invoke
+ Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
+ }
+
+ [Fact]
+ public void GeminiPromptExecutionSettingsWithNoFunctionChoiceBehaviorDoesNotSetToolCallBehavior()
+ {
+ // Arrange
+ var settings = new GeminiPromptExecutionSettings();
+
+ // Act
+ var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
+
+ // Assert
+ Assert.Null(converted.ToolCallBehavior);
+ }
+
+ [Fact]
+ public void GeminiPromptExecutionSettingsPreservesExistingToolCallBehavior()
+ {
+ // Arrange
+ var settings = new GeminiPromptExecutionSettings
+ {
+ ToolCallBehavior = GeminiToolCallBehavior.EnableKernelFunctions,
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
+ };
+
+ // Act
+ var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
+
+ // Assert - ToolCallBehavior should be preserved when already set
+ Assert.NotNull(converted.ToolCallBehavior);
+ Assert.IsType(converted.ToolCallBehavior);
+ Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
+ }
+
private static KernelPlugin GetTestPlugin()
{
var function = KernelFunctionFactory.CreateFromMethod(
diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs
index 5f8bc0874cc2..a9729f518899 100644
--- a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs
+++ b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs
@@ -188,6 +188,9 @@ public IDictionary? Labels
/// the function, and sending back the result. The intermediate messages will be retained in the
/// if an instance was provided.
///
+ ///
+ /// This property is deprecated. Use instead.
+ ///
public GeminiToolCallBehavior? ToolCallBehavior
{
get => this._toolCallBehavior;
@@ -357,11 +360,71 @@ public static GeminiPromptExecutionSettings FromExecutionSettings(PromptExecutio
{
case null:
return new GeminiPromptExecutionSettings();
- case GeminiPromptExecutionSettings settings:
- return settings;
+ case GeminiPromptExecutionSettings geminiSettings:
+ // If FunctionChoiceBehavior is set and ToolCallBehavior is not, convert it
+ if (geminiSettings.FunctionChoiceBehavior is not null && geminiSettings.ToolCallBehavior is null)
+ {
+ geminiSettings.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(geminiSettings.FunctionChoiceBehavior);
+ }
+ return geminiSettings;
}
var json = JsonSerializer.Serialize(executionSettings);
- return JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive)!;
+ var settings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive)!;
+
+ // If FunctionChoiceBehavior is set and ToolCallBehavior is not, convert it
+ if (executionSettings.FunctionChoiceBehavior is not null && settings.ToolCallBehavior is null)
+ {
+ settings.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(executionSettings.FunctionChoiceBehavior);
+ }
+
+ return settings;
+ }
+
+ ///
+ /// Shared empty kernel instance used for FunctionChoiceBehavior conversion.
+ ///
+ private static readonly Kernel s_emptyKernel = new();
+
+ ///
+ /// Converts a to a .
+ ///
+ /// The to convert.
+ /// The converted .
+ internal static GeminiToolCallBehavior? ConvertFunctionChoiceBehaviorToToolCallBehavior(FunctionChoiceBehavior? functionChoiceBehavior)
+ {
+ if (functionChoiceBehavior is null)
+ {
+ return null;
+ }
+
+ // Check the type and determine auto-invoke by reflection or known behavior types
+ // All FunctionChoiceBehavior types (Auto, Required, None) support auto-invoke
+ // We use a simple approach: get the configuration with minimal context to check AutoInvoke
+ try
+ {
+ var context = new FunctionChoiceBehaviorConfigurationContext(new ChatHistory())
+ {
+ Kernel = s_emptyKernel, // Provide an empty kernel for the configuration
+ RequestSequenceIndex = 0
+ };
+ var config = functionChoiceBehavior.GetConfiguration(context);
+
+ // Return appropriate GeminiToolCallBehavior based on AutoInvoke setting
+ if (config.AutoInvoke)
+ {
+ return GeminiToolCallBehavior.AutoInvokeKernelFunctions;
+ }
+
+ return GeminiToolCallBehavior.EnableKernelFunctions;
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch
+#pragma warning restore CA1031
+ {
+ // If we can't get configuration (e.g., due to missing dependencies or unexpected state),
+ // default to EnableKernelFunctions as the safer option that doesn't auto-invoke
+ return GeminiToolCallBehavior.EnableKernelFunctions;
+ }
}
}
diff --git a/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiChatMessageContent.cs b/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiChatMessageContent.cs
index ac5f6ced3e8b..6154decf12d8 100644
--- a/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiChatMessageContent.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiChatMessageContent.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -38,6 +39,16 @@ public GeminiChatMessageContent(GeminiFunctionToolResult calledToolResult)
Verify.NotNull(calledToolResult);
this.CalledToolResult = calledToolResult;
+
+ // Parse plugin and function names from FullyQualifiedName
+ var (pluginName, functionName) = ParseFullyQualifiedName(calledToolResult.FullyQualifiedName);
+
+ // Also populate Items collection with FunctionResultContent for compatibility with FunctionChoiceBehavior
+ this.Items.Add(new FunctionResultContent(
+ functionName: functionName,
+ pluginName: pluginName,
+ callId: null, // Gemini doesn't provide call IDs
+ result: calledToolResult.FunctionResult));
}
///
@@ -63,6 +74,35 @@ internal GeminiChatMessageContent(
metadata: metadata)
{
this.CalledToolResult = calledToolResult;
+
+ // Also populate Items collection with FunctionResultContent for compatibility with FunctionChoiceBehavior
+ if (calledToolResult is not null)
+ {
+ // Parse plugin and function names from FullyQualifiedName
+ var (pluginName, functionName) = ParseFullyQualifiedName(calledToolResult.FullyQualifiedName);
+
+ this.Items.Add(new FunctionResultContent(
+ functionName: functionName,
+ pluginName: pluginName,
+ callId: null, // Gemini doesn't provide call IDs
+ result: calledToolResult.FunctionResult));
+ }
+ }
+
+ ///
+ /// Parses a fully qualified function name into plugin name and function name.
+ ///
+ private static (string? PluginName, string FunctionName) ParseFullyQualifiedName(string fullyQualifiedName)
+ {
+ int separatorPos = fullyQualifiedName.IndexOf(GeminiFunction.NameSeparator, StringComparison.Ordinal);
+ if (separatorPos >= 0)
+ {
+ string pluginName = fullyQualifiedName.Substring(0, separatorPos).Trim();
+ string functionName = fullyQualifiedName.Substring(separatorPos + GeminiFunction.NameSeparator.Length).Trim();
+ return (pluginName, functionName);
+ }
+
+ return (null, fullyQualifiedName);
}
///
@@ -88,6 +128,29 @@ internal GeminiChatMessageContent(
metadata: metadata)
{
this.ToolCalls = functionsToolCalls?.Select(tool => new GeminiFunctionToolCall(tool)).ToList();
+
+ // Also populate Items collection with FunctionCallContent for compatibility with FunctionChoiceBehavior
+ if (this.ToolCalls is not null)
+ {
+ foreach (var toolCall in this.ToolCalls)
+ {
+ KernelArguments? arguments = null;
+ if (toolCall.Arguments is not null)
+ {
+ arguments = new KernelArguments();
+ foreach (var arg in toolCall.Arguments)
+ {
+ arguments[arg.Key] = arg.Value;
+ }
+ }
+
+ this.Items.Add(new FunctionCallContent(
+ functionName: toolCall.FunctionName,
+ pluginName: toolCall.PluginName,
+ id: null, // Gemini doesn't provide call IDs
+ arguments: arguments));
+ }
+ }
}
///