mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
.Net: Honor FunctionChoiceBehavior function list in Gemini connector (#14183)
## Motivation and Context The Gemini connector converts a `FunctionChoiceBehavior` (FCB) into the connector-internal `GeminiToolCallBehavior` in `GeminiPromptExecutionSettings`. That conversion only inspected the `AutoInvoke` flag and evaluated the behavior against an empty kernel, so the configured function list and choice were dropped: - `FunctionChoiceBehavior.Auto()` / `Auto([subset])` always provided **all** kernel functions to the model, regardless of the list supplied. - `FunctionChoiceBehavior.Auto([])`, which is documented as equivalent to disabling function calling, still provided all kernel functions. - Explicit function lists failed to convert because the behavior was evaluated against an empty kernel. This diverges from the other connectors, which resolve the behavior against the request kernel and provide exactly the functions it specifies. ## Description - Resolve the `FunctionChoiceBehavior` against the request's kernel and provide exactly the functions it declares (all, an explicit subset, or none), matching the shared function-calling semantics used by the other connectors. - An empty function list now correctly disables function calling. - Perform the conversion on a clone so shared or frozen settings instances are not mutated and the resolved result is not cached across different kernels. - Add a kernel-aware `FromExecutionSettings(settings, kernel)` overload and use it from `GeminiChatCompletionClient`. ## Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Copilot <[email protected]> Copilot-Session: c0e4edcd-4d36-486f-950b-9c0c36fa68e8
This commit is contained in:
+69
@@ -159,6 +159,75 @@ public sealed class GeminiChatClientFunctionCallingTests : IDisposable
|
||||
Assert.IsAssignableFrom<IChatClient>(chatClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoInvokeDoesNotInvokeFunctionThatWasNotAdvertisedByFunctionChoiceBehaviorAsync()
|
||||
{
|
||||
// Arrange - register two functions in the same kernel but only advertise one of them via the
|
||||
// FunctionChoiceBehavior. The model is then made to request the function that was not advertised.
|
||||
bool unadvertisedFunctionInvoked = false;
|
||||
var plugin = KernelPluginFactory.CreateFromFunctions("TestPlugin", new[]
|
||||
{
|
||||
KernelFunctionFactory.CreateFromMethod(() => "allowed-result", "AllowedFunction"),
|
||||
KernelFunctionFactory.CreateFromMethod(() => { unadvertisedFunctionInvoked = true; return "secret"; }, "UnadvertisedFunction"),
|
||||
});
|
||||
var kernel = new Kernel();
|
||||
kernel.Plugins.Add(plugin);
|
||||
|
||||
var allowedFunction = kernel.Plugins.GetFunction("TestPlugin", "AllowedFunction");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: [allowedFunction], autoInvoke: true)
|
||||
};
|
||||
|
||||
// The model requests the function that was not advertised, then produces a final text answer.
|
||||
string functionCallResponse = $$"""
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [ { "functionCall": { "name": "TestPlugin{{GeminiFunction.NameSeparator}}UnadvertisedFunction", "args": {} } } ],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"usageMetadata": { "promptTokenCount": 1, "candidatesTokenCount": 1, "totalTokenCount": 2 }
|
||||
}
|
||||
""";
|
||||
string finalTextResponse = """
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"content": { "parts": [ { "text": "done" } ], "role": "model" },
|
||||
"finishReason": "STOP",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"usageMetadata": { "promptTokenCount": 1, "candidatesTokenCount": 1, "totalTokenCount": 2 }
|
||||
}
|
||||
""";
|
||||
using var functionCallHttpResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(functionCallResponse) };
|
||||
this._messageHandlerStub.ResponseQueue.Enqueue(functionCallHttpResponse);
|
||||
|
||||
using var finalTextHttpResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(finalTextResponse) };
|
||||
this._messageHandlerStub.ResponseQueue.Enqueue(finalTextHttpResponse);
|
||||
|
||||
var service = this.CreateChatCompletionService();
|
||||
var chatHistory = new ChatHistory();
|
||||
chatHistory.AddUserMessage("Do something");
|
||||
|
||||
// Act
|
||||
var result = await service.GetChatMessageContentsAsync(chatHistory, settings, kernel);
|
||||
|
||||
// Assert - the unadvertised function must never be invoked, even though it exists in the kernel.
|
||||
Assert.False(unadvertisedFunctionInvoked);
|
||||
// The requested tool call was processed and rejected (not silently ignored): both responses were
|
||||
// consumed and the model's final text answer was returned.
|
||||
Assert.Empty(this._messageHandlerStub.ResponseQueue);
|
||||
Assert.Contains(result, m => m.Content is not null && m.Content.Contains("done", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private GoogleAIGeminiChatCompletionService CreateChatCompletionService(HttpClient? httpClient = null)
|
||||
{
|
||||
return new GoogleAIGeminiChatCompletionService(
|
||||
|
||||
@@ -195,78 +195,210 @@ public sealed class GeminiToolCallBehaviorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FunctionChoiceBehaviorAutoConvertsToAutoInvokeKernelFunctions()
|
||||
public void FunctionChoiceBehaviorAutoConvertsToEnabledFunctionsWithAllKernelFunctions()
|
||||
{
|
||||
// Arrange
|
||||
var kernel = CreateKernelWithFunctions("FunctionA", "FunctionB");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
|
||||
// Act
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(converted.ToolCallBehavior);
|
||||
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
|
||||
var enabledFunctions = Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
|
||||
Assert.True(converted.ToolCallBehavior.MaximumAutoInvokeAttempts > 0);
|
||||
// The provided set is validated against the requested functions.
|
||||
Assert.False(GetAllowAnyRequestedKernelFunction(converted.ToolCallBehavior));
|
||||
// Auto continues to provide the functions across follow-up requests.
|
||||
Assert.Equal(int.MaxValue, converted.ToolCallBehavior.MaximumUseAttempts);
|
||||
Assert.Equal(new[] { $"TestPlugin{GeminiFunction.NameSeparator}FunctionA", $"TestPlugin{GeminiFunction.NameSeparator}FunctionB" }, GetAdvertisedFunctionNames(enabledFunctions, kernel));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FunctionChoiceBehaviorAutoWithNoAutoInvokeConvertsToEnableKernelFunctions()
|
||||
public void FunctionChoiceBehaviorAutoWithNoAutoInvokeConvertsToEnabledFunctionsWithoutAutoInvoke()
|
||||
{
|
||||
// Arrange
|
||||
var kernel = CreateKernelWithFunctions("FunctionA");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
|
||||
};
|
||||
|
||||
// Act
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(converted.ToolCallBehavior);
|
||||
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
|
||||
Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
|
||||
Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FunctionChoiceBehaviorRequiredConvertsToAutoInvokeKernelFunctions()
|
||||
public void FunctionChoiceBehaviorRequiredConvertsToEnabledFunctions()
|
||||
{
|
||||
// Arrange
|
||||
var kernel = CreateKernelWithFunctions("FunctionA");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Required()
|
||||
};
|
||||
|
||||
// Act
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(converted.ToolCallBehavior);
|
||||
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
|
||||
Assert.True(converted.ToolCallBehavior.MaximumAutoInvokeAttempts > 0);
|
||||
Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
|
||||
Assert.False(GetAllowAnyRequestedKernelFunction(converted.ToolCallBehavior));
|
||||
// Required stops providing the functions after the first request so the model is not
|
||||
// repeatedly forced to call them on follow-up iterations. Auto-invocation is capped to the same
|
||||
// value so it does not outlast the advertised tools (MaximumUseAttempts >= MaximumAutoInvokeAttempts).
|
||||
Assert.Equal(1, converted.ToolCallBehavior.MaximumUseAttempts);
|
||||
Assert.Equal(1, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
|
||||
Assert.True(converted.ToolCallBehavior.MaximumUseAttempts >= converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FunctionChoiceBehaviorNoneConvertsToEnableKernelFunctions()
|
||||
public void FunctionChoiceBehaviorNoneConvertsToEnabledFunctionsWithoutAutoInvoke()
|
||||
{
|
||||
// Arrange
|
||||
var kernel = CreateKernelWithFunctions("FunctionA");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.None()
|
||||
};
|
||||
|
||||
// Act
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(converted.ToolCallBehavior);
|
||||
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
|
||||
Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
|
||||
// None behavior doesn't auto-invoke
|
||||
Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FunctionChoiceBehaviorAutoWithEmptyFunctionListDisablesFunctionCalling()
|
||||
{
|
||||
// Arrange
|
||||
// An empty function list is documented as being equivalent to disabling function calling.
|
||||
var kernel = CreateKernelWithFunctions("FunctionA", "FunctionB");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: [], autoInvoke: true)
|
||||
};
|
||||
|
||||
// Act
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
|
||||
|
||||
// Assert
|
||||
Assert.Null(converted.ToolCallBehavior);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FunctionChoiceBehaviorAutoWithSubsetProvidesOnlyThatSubset()
|
||||
{
|
||||
// Arrange
|
||||
var kernel = CreateKernelWithFunctions("First", "Second");
|
||||
var first = kernel.Plugins.GetFunction("TestPlugin", "First");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: [first], autoInvoke: true)
|
||||
};
|
||||
|
||||
// Act
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(converted.ToolCallBehavior);
|
||||
var enabledFunctions = Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
|
||||
Assert.False(GetAllowAnyRequestedKernelFunction(converted.ToolCallBehavior));
|
||||
// Only the function specified in the behavior is provided to the model.
|
||||
Assert.Equal(new[] { $"TestPlugin{GeminiFunction.NameSeparator}First" }, GetAdvertisedFunctionNames(enabledFunctions, kernel));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FunctionChoiceBehaviorConfigurationErrorsArePropagated()
|
||||
{
|
||||
// Arrange - auto-invocation requires the declared function to exist in the kernel; when it does
|
||||
// not, the behavior resolution throws and the error must surface rather than be swallowed.
|
||||
var kernelWithFunction = CreateKernelWithFunctions("First");
|
||||
var first = kernelWithFunction.Plugins.GetFunction("TestPlugin", "First");
|
||||
var emptyKernel = Kernel.CreateBuilder().Build();
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: [first], autoInvoke: true)
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<KernelException>(() => GeminiPromptExecutionSettings.FromExecutionSettings(settings, emptyKernel));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromExecutionSettingsDoesNotMutateCallerSettingsWhenConvertingFunctionChoiceBehavior()
|
||||
{
|
||||
// Arrange
|
||||
var kernel = CreateKernelWithFunctions("FunctionA");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
|
||||
// Act
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
|
||||
|
||||
// Assert - the caller's original settings object must remain untouched.
|
||||
Assert.NotSame(settings, converted);
|
||||
Assert.Null(settings.ToolCallBehavior);
|
||||
Assert.NotNull(converted.ToolCallBehavior);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromExecutionSettingsWithFrozenSettingsDoesNotThrowWhenConvertingFunctionChoiceBehavior()
|
||||
{
|
||||
// Arrange
|
||||
var kernel = CreateKernelWithFunctions("FunctionA");
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
settings.Freeze();
|
||||
|
||||
// Act - converting a frozen settings object must not attempt to mutate it.
|
||||
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(converted.ToolCallBehavior);
|
||||
Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromExecutionSettingsReResolvesFunctionChoiceBehaviorPerKernel()
|
||||
{
|
||||
// Arrange - a single reused settings object must reflect each request's own kernel.
|
||||
var settings = new GeminiPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
var kernelA = CreateKernelWithFunctions("FunctionA");
|
||||
var kernelB = CreateKernelWithFunctions("FunctionB");
|
||||
|
||||
// Act
|
||||
var convertedA = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernelA);
|
||||
var convertedB = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernelB);
|
||||
|
||||
// Assert - each request reflects its own kernel.
|
||||
var enabledA = Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(convertedA.ToolCallBehavior);
|
||||
var enabledB = Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(convertedB.ToolCallBehavior);
|
||||
Assert.Equal(new[] { $"TestPlugin{GeminiFunction.NameSeparator}FunctionA" }, GetAdvertisedFunctionNames(enabledA, kernelA));
|
||||
Assert.Equal(new[] { $"TestPlugin{GeminiFunction.NameSeparator}FunctionB" }, GetAdvertisedFunctionNames(enabledB, kernelB));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeminiPromptExecutionSettingsWithNoFunctionChoiceBehaviorDoesNotSetToolCallBehavior()
|
||||
{
|
||||
@@ -311,6 +443,32 @@ public sealed class GeminiToolCallBehaviorTests
|
||||
return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]);
|
||||
}
|
||||
|
||||
private static Kernel CreateKernelWithFunctions(params string[] functionNames)
|
||||
{
|
||||
var functions = functionNames
|
||||
.Select(name => KernelFunctionFactory.CreateFromMethod(() => "result", name))
|
||||
.ToArray();
|
||||
|
||||
var kernel = Kernel.CreateBuilder().Build();
|
||||
kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("TestPlugin", functions));
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private static bool GetAllowAnyRequestedKernelFunction(GeminiToolCallBehavior behavior)
|
||||
=> behavior.AllowAnyRequestedKernelFunction;
|
||||
|
||||
private static string[] GetAdvertisedFunctionNames(GeminiToolCallBehavior behavior, Kernel kernel)
|
||||
{
|
||||
var request = new GeminiRequest();
|
||||
behavior.ConfigureGeminiRequest(kernel, request);
|
||||
if (request.Tools is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return request.Tools[0].Functions.Select(f => f.Name).ToArray();
|
||||
}
|
||||
|
||||
private static void AssertFunctions(GeminiRequest request)
|
||||
{
|
||||
Assert.NotNull(request.Tools);
|
||||
|
||||
+1
-1
@@ -323,7 +323,7 @@ internal sealed class GeminiChatCompletionClient : ClientBase
|
||||
{
|
||||
ValidateChatHistory(chatHistory);
|
||||
|
||||
var geminiExecutionSettings = GeminiPromptExecutionSettings.FromExecutionSettings(executionSettings);
|
||||
var geminiExecutionSettings = GeminiPromptExecutionSettings.FromExecutionSettings(executionSettings, kernel);
|
||||
ValidateMaxTokens(geminiExecutionSettings.MaxTokens);
|
||||
|
||||
if (this.Logger.IsEnabled(LogLevel.Trace))
|
||||
|
||||
@@ -355,16 +355,36 @@ public sealed class GeminiPromptExecutionSettings : PromptExecutionSettings
|
||||
/// is null, an <see cref="ArgumentException"/> is thrown.
|
||||
/// </returns>
|
||||
public static GeminiPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings)
|
||||
=> FromExecutionSettings(executionSettings, kernel: null);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new settings object with the values from another settings object, resolving any
|
||||
/// <see cref="FunctionChoiceBehavior"/> against the provided <paramref name="kernel"/>.
|
||||
/// </summary>
|
||||
/// <param name="executionSettings">The prompt execution settings to convert.</param>
|
||||
/// <param name="kernel">
|
||||
/// The <see cref="Kernel"/> used to resolve the functions declared by a <see cref="FunctionChoiceBehavior"/>.
|
||||
/// This must be the kernel that will actually be used for the request so that the advertised function set
|
||||
/// matches the caller's intent.
|
||||
/// </param>
|
||||
/// <returns>The converted <see cref="GeminiPromptExecutionSettings"/>.</returns>
|
||||
public static GeminiPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings, Kernel? kernel)
|
||||
{
|
||||
switch (executionSettings)
|
||||
{
|
||||
case null:
|
||||
return new GeminiPromptExecutionSettings();
|
||||
case GeminiPromptExecutionSettings geminiSettings:
|
||||
// If FunctionChoiceBehavior is set and ToolCallBehavior is not, convert it
|
||||
// If FunctionChoiceBehavior is set and ToolCallBehavior is not, resolve it against the
|
||||
// request's kernel. This is done on a clone so we neither mutate the caller's (possibly
|
||||
// shared or frozen) settings instance nor cache a kernel-specific result that could go
|
||||
// stale when the same settings object is reused with a different kernel.
|
||||
if (geminiSettings.FunctionChoiceBehavior is not null && geminiSettings.ToolCallBehavior is null)
|
||||
{
|
||||
geminiSettings.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(geminiSettings.FunctionChoiceBehavior);
|
||||
var resolved = (GeminiPromptExecutionSettings)geminiSettings.Clone();
|
||||
resolved.FunctionChoiceBehavior = geminiSettings.FunctionChoiceBehavior;
|
||||
resolved.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(geminiSettings.FunctionChoiceBehavior, kernel);
|
||||
return resolved;
|
||||
}
|
||||
return geminiSettings;
|
||||
}
|
||||
@@ -375,56 +395,62 @@ public sealed class GeminiPromptExecutionSettings : PromptExecutionSettings
|
||||
// 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);
|
||||
settings.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(executionSettings.FunctionChoiceBehavior, kernel);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared empty kernel instance used for FunctionChoiceBehavior conversion.
|
||||
/// </summary>
|
||||
private static readonly Kernel s_emptyKernel = new();
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="FunctionChoiceBehavior"/> to a <see cref="GeminiToolCallBehavior"/>.
|
||||
/// </summary>
|
||||
/// <param name="functionChoiceBehavior">The <see cref="FunctionChoiceBehavior"/> to convert.</param>
|
||||
/// <param name="kernel">The <see cref="Kernel"/> used to resolve the behavior's declared functions.</param>
|
||||
/// <returns>The converted <see cref="GeminiToolCallBehavior"/>.</returns>
|
||||
internal static GeminiToolCallBehavior? ConvertFunctionChoiceBehaviorToToolCallBehavior(FunctionChoiceBehavior? functionChoiceBehavior)
|
||||
/// <remarks>
|
||||
/// The conversion honors the exact function set expressed by the <see cref="FunctionChoiceBehavior"/>.
|
||||
/// The resolved <see cref="FunctionChoiceBehaviorConfiguration.Functions"/> list is the set of functions
|
||||
/// provided to the model, matching the behavior of the other connectors. A null or empty function set
|
||||
/// (e.g. <c>FunctionChoiceBehavior.Auto([])</c>, which is documented as being equivalent to disabling
|
||||
/// function calling) results in no functions being provided. Any configuration errors thrown while
|
||||
/// resolving the behavior (for example, requesting auto-invocation for a function that is not present in
|
||||
/// the kernel) are surfaced to the caller rather than being swallowed, matching the other connectors.
|
||||
/// </remarks>
|
||||
internal static GeminiToolCallBehavior? ConvertFunctionChoiceBehaviorToToolCallBehavior(FunctionChoiceBehavior? functionChoiceBehavior, Kernel? kernel)
|
||||
{
|
||||
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
|
||||
// Resolve the behavior against the kernel so the provided function set reflects
|
||||
// exactly what the caller requested (all functions, an explicit subset, or none).
|
||||
var context = new FunctionChoiceBehaviorConfigurationContext(new ChatHistory())
|
||||
{
|
||||
var context = new FunctionChoiceBehaviorConfigurationContext(new ChatHistory())
|
||||
{
|
||||
Kernel = s_emptyKernel, // Provide an empty kernel for the configuration
|
||||
RequestSequenceIndex = 0
|
||||
};
|
||||
var config = functionChoiceBehavior.GetConfiguration(context);
|
||||
Kernel = kernel,
|
||||
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
|
||||
// A null or empty resolved function list means no functions should be provided to the model,
|
||||
// which preserves the documented "empty list disables function calling" behavior.
|
||||
if (config.Functions is not { Count: > 0 } functions)
|
||||
{
|
||||
// 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;
|
||||
return null;
|
||||
}
|
||||
|
||||
// For the Required choice, functions should only be provided on the first request to avoid
|
||||
// repeatedly forcing the model to call them on follow-up iterations, matching the shared
|
||||
// FunctionChoiceBehavior semantics. Limiting the use attempts to 1 stops the connector from
|
||||
// re-advertising the tools after the initial request; the auto-invoke attempts are capped to the
|
||||
// same value (see GeminiToolCallBehavior) so a single forced call is executed and the model's
|
||||
// final answer is returned.
|
||||
int maximumUseAttempts = config.Choice == FunctionChoice.Required ? 1 : int.MaxValue;
|
||||
|
||||
// Provide exactly the resolved functions.
|
||||
return new GeminiToolCallBehavior.EnabledFunctions(
|
||||
functions.Select(f => f.Metadata.ToGeminiFunction()),
|
||||
autoInvoke: config.AutoInvoke,
|
||||
maximumUseAttempts: maximumUseAttempts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +68,15 @@ public abstract class GeminiToolCallBehavior
|
||||
}
|
||||
|
||||
/// <summary>Initializes the instance; prevents external instantiation.</summary>
|
||||
private GeminiToolCallBehavior(bool autoInvoke)
|
||||
private GeminiToolCallBehavior(bool autoInvoke, int maximumUseAttempts = int.MaxValue)
|
||||
{
|
||||
this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0;
|
||||
this.MaximumUseAttempts = maximumUseAttempts;
|
||||
|
||||
// Auto-invocation can never outlast the number of requests that still advertise the tools, otherwise
|
||||
// the model could request a tool after it has been removed from the request. Cap the auto-invoke
|
||||
// attempts by the use attempts to preserve the documented invariant (MaximumUseAttempts >= MaximumAutoInvokeAttempts).
|
||||
var autoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0;
|
||||
this.MaximumAutoInvokeAttempts = System.Math.Min(autoInvokeAttempts, maximumUseAttempts);
|
||||
}
|
||||
|
||||
/// <summary>Gets how many requests are part of a single interaction should include this tool in the request.</summary>
|
||||
@@ -80,7 +86,7 @@ public abstract class GeminiToolCallBehavior
|
||||
/// if this is 1, the first request will include the tools, but the subsequent response sending back the tool's result
|
||||
/// will not include the tools for further use.
|
||||
/// </remarks>
|
||||
public int MaximumUseAttempts { get; } = int.MaxValue;
|
||||
public int MaximumUseAttempts { get; }
|
||||
|
||||
/// <summary>Gets how many tool call request/response roundtrips are supported with auto-invocation.</summary>
|
||||
/// <remarks>
|
||||
@@ -137,9 +143,15 @@ public abstract class GeminiToolCallBehavior
|
||||
/// <summary>
|
||||
/// Represents a <see cref="GeminiToolCallBehavior"/> that provides a specified list of functions to the model.
|
||||
/// </summary>
|
||||
internal sealed class EnabledFunctions(IEnumerable<GeminiFunction> functions, bool autoInvoke) : GeminiToolCallBehavior(autoInvoke)
|
||||
internal sealed class EnabledFunctions : GeminiToolCallBehavior
|
||||
{
|
||||
private readonly GeminiFunction[] _functions = functions.ToArray();
|
||||
private readonly GeminiFunction[] _functions;
|
||||
|
||||
internal EnabledFunctions(IEnumerable<GeminiFunction> functions, bool autoInvoke, int maximumUseAttempts = int.MaxValue)
|
||||
: base(autoInvoke, maximumUseAttempts)
|
||||
{
|
||||
this._functions = functions.ToArray();
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): " +
|
||||
|
||||
Reference in New Issue
Block a user