Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<GeminiToolCallBehavior.KernelFunctions>(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<GeminiToolCallBehavior.KernelFunctions>(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<GeminiToolCallBehavior.KernelFunctions>(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<GeminiToolCallBehavior.KernelFunctions>(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<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
}

private static KernelPlugin GetTestPlugin()
{
var function = KernelFunctionFactory.CreateFromMethod(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ public IDictionary<string, string>? Labels
/// the function, and sending back the result. The intermediate messages will be retained in the
/// <see cref="ChatHistory"/> if an instance was provided.
/// </remarks>
/// <remarks>
/// This property is deprecated. Use <see cref="PromptExecutionSettings.FunctionChoiceBehavior"/> instead.
/// </remarks>
public GeminiToolCallBehavior? ToolCallBehavior
{
get => this._toolCallBehavior;
Expand Down Expand Up @@ -357,11 +360,72 @@ 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<GeminiPromptExecutionSettings>(json, JsonOptionsCache.ReadPermissive)!;
var settings = JsonSerializer.Deserialize<GeminiPromptExecutionSettings>(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;
}

/// <summary>
/// Converts a <see cref="FunctionChoiceBehavior"/> to a <see cref="GeminiToolCallBehavior"/>.
/// </summary>
/// <param name="functionChoiceBehavior">The <see cref="FunctionChoiceBehavior"/> to convert.</param>
/// <returns>The converted <see cref="GeminiToolCallBehavior"/>.</returns>
internal static GeminiToolCallBehavior? ConvertFunctionChoiceBehaviorToToolCallBehavior(FunctionChoiceBehavior? functionChoiceBehavior)
{
if (functionChoiceBehavior is null)
{
return null;
}

// Static empty kernel to avoid creating new instances for each conversion
static Kernel GetEmptyKernel()
{
return new Kernel();
}

// 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 = GetEmptyKernel(), // 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -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));
}

/// <summary>
Expand All @@ -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));
}
}

/// <summary>
/// Parses a fully qualified function name into plugin name and function name.
/// </summary>
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);
}

/// <summary>
Expand All @@ -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));
}
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SemanticKernel.ChatCompletion;
Expand Down
Loading
Loading