Skip to content

Commit cecee4c

Browse files
authored
.Net: Fixes MCP Behavior for AIFunction.AsKernelFunction() + Fix ensuring FunctionChoiceBehavior works correctly with IChatClients. (#12226)
### Motivation and Context - Fixes #12209 - Fixes #12217
1 parent 912a7bb commit cecee4c

12 files changed

+2376
-25
lines changed

dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatClient_AutoFunctionChoiceBehaviorTests.cs

Lines changed: 503 additions & 0 deletions
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.ComponentModel;
6+
using System.Globalization;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Azure.Identity;
10+
using Microsoft.Extensions.AI;
11+
using Microsoft.Extensions.Configuration;
12+
using Microsoft.SemanticKernel;
13+
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
14+
using SemanticKernel.IntegrationTests.TestSettings;
15+
using Xunit;
16+
17+
namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI;
18+
19+
public sealed class AzureOpenAIChatClientNoneFunctionChoiceBehaviorTests : BaseIntegrationTest
20+
{
21+
private readonly Kernel _kernel;
22+
private readonly FakeFunctionFilter _autoFunctionInvocationFilter;
23+
private readonly IChatClient _chatClient;
24+
25+
public AzureOpenAIChatClientNoneFunctionChoiceBehaviorTests()
26+
{
27+
this._autoFunctionInvocationFilter = new FakeFunctionFilter();
28+
29+
this._kernel = this.InitializeKernel();
30+
this._kernel.AutoFunctionInvocationFilters.Add(this._autoFunctionInvocationFilter);
31+
this._chatClient = this._kernel.GetRequiredService<IChatClient>();
32+
}
33+
34+
[Fact]
35+
public async Task SpecifiedInCodeInstructsConnectorNotToInvokeKernelFunctionAsync()
36+
{
37+
// Arrange
38+
var plugin = this._kernel.CreatePluginFromType<DateTimeUtils>();
39+
this._kernel.Plugins.Add(plugin);
40+
41+
var invokedFunctions = new List<string>();
42+
43+
this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) =>
44+
{
45+
invokedFunctions.Add(context.Function.Name);
46+
await next(context);
47+
});
48+
49+
// Act
50+
var settings = new AzureOpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
51+
var chatOptions = settings.ToChatOptions(this._kernel);
52+
53+
var messages = new List<ChatMessage>
54+
{
55+
new(ChatRole.User, "How many days until Christmas?")
56+
};
57+
58+
var response = await this._chatClient.GetResponseAsync(messages, chatOptions);
59+
60+
// Assert
61+
Assert.NotNull(response);
62+
63+
Assert.Empty(invokedFunctions);
64+
}
65+
66+
[Fact]
67+
public async Task SpecifiedInPromptInstructsConnectorNotToInvokeKernelFunctionAsync()
68+
{
69+
// Arrange
70+
this._kernel.ImportPluginFromType<DateTimeUtils>();
71+
72+
var invokedFunctions = new List<string>();
73+
74+
this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) =>
75+
{
76+
invokedFunctions.Add(context.Function.Name);
77+
await next(context);
78+
});
79+
80+
var promptTemplate = """"
81+
template_format: semantic-kernel
82+
template: How many days until Christmas?
83+
execution_settings:
84+
default:
85+
temperature: 0.1
86+
function_choice_behavior:
87+
type: none
88+
"""";
89+
90+
var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate);
91+
92+
// Act
93+
var result = await this._kernel.InvokeAsync(promptFunction);
94+
95+
// Assert
96+
Assert.NotNull(result);
97+
98+
Assert.Empty(invokedFunctions);
99+
}
100+
101+
[Fact]
102+
public async Task SpecifiedInCodeInstructsConnectorNotToInvokeKernelFunctionForStreamingAsync()
103+
{
104+
// Arrange
105+
var plugin = this._kernel.CreatePluginFromType<DateTimeUtils>();
106+
this._kernel.Plugins.Add(plugin);
107+
108+
var invokedFunctions = new List<string>();
109+
110+
this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) =>
111+
{
112+
invokedFunctions.Add(context.Function.Name);
113+
await next(context);
114+
});
115+
116+
var settings = new AzureOpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
117+
var chatOptions = settings.ToChatOptions(this._kernel);
118+
119+
var messages = new List<ChatMessage>
120+
{
121+
new(ChatRole.User, "How many days until Christmas?")
122+
};
123+
124+
StringBuilder result = new();
125+
126+
// Act
127+
await foreach (var update in this._chatClient.GetStreamingResponseAsync(messages, chatOptions))
128+
{
129+
foreach (var content in update.Contents)
130+
{
131+
if (content is Microsoft.Extensions.AI.TextContent textContent)
132+
{
133+
result.Append(textContent.Text);
134+
}
135+
}
136+
}
137+
138+
// Assert
139+
Assert.NotNull(result);
140+
141+
Assert.Empty(invokedFunctions);
142+
}
143+
144+
[Fact]
145+
public async Task SpecifiedInPromptInstructsConnectorNotToInvokeKernelFunctionForStreamingAsync()
146+
{
147+
// Arrange
148+
this._kernel.ImportPluginFromType<DateTimeUtils>();
149+
150+
var invokedFunctions = new List<string>();
151+
152+
this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) =>
153+
{
154+
invokedFunctions.Add(context.Function.Name);
155+
await next(context);
156+
});
157+
158+
var promptTemplate = """"
159+
template_format: semantic-kernel
160+
template: How many days until Christmas?
161+
execution_settings:
162+
default:
163+
temperature: 0.1
164+
function_choice_behavior:
165+
type: none
166+
"""";
167+
168+
var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate);
169+
170+
StringBuilder result = new();
171+
172+
// Act
173+
await foreach (string update in promptFunction.InvokeStreamingAsync<string>(this._kernel))
174+
{
175+
result.Append(update);
176+
}
177+
178+
// Assert
179+
Assert.NotNull(result);
180+
181+
Assert.Empty(invokedFunctions);
182+
}
183+
184+
private Kernel InitializeKernel()
185+
{
186+
var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get<AzureOpenAIConfiguration>();
187+
Assert.NotNull(azureOpenAIConfiguration);
188+
Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName);
189+
Assert.NotNull(azureOpenAIConfiguration.Endpoint);
190+
191+
var kernelBuilder = base.CreateKernelBuilder();
192+
193+
kernelBuilder.AddAzureOpenAIChatClient(
194+
deploymentName: azureOpenAIConfiguration.ChatDeploymentName,
195+
modelId: azureOpenAIConfiguration.ChatModelId,
196+
endpoint: azureOpenAIConfiguration.Endpoint,
197+
credentials: new AzureCliCredential());
198+
199+
return kernelBuilder.Build();
200+
}
201+
202+
private readonly IConfigurationRoot _configuration = new ConfigurationBuilder()
203+
.AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true)
204+
.AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
205+
.AddEnvironmentVariables()
206+
.AddUserSecrets<AzureOpenAIChatClientNoneFunctionChoiceBehaviorTests>()
207+
.Build();
208+
209+
/// <summary>
210+
/// A plugin that returns the current time.
211+
/// </summary>
212+
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
213+
private sealed class DateTimeUtils
214+
#pragma warning restore CA1812 // Avoid uninstantiated internal classes
215+
{
216+
[KernelFunction]
217+
[Description("Retrieves the current date.")]
218+
public string GetCurrentDate() => DateTime.UtcNow.ToString("d", CultureInfo.InvariantCulture);
219+
}
220+
221+
#region private
222+
223+
private sealed class FakeFunctionFilter : IAutoFunctionInvocationFilter
224+
{
225+
private Func<AutoFunctionInvocationContext, Func<AutoFunctionInvocationContext, Task>, Task>? _onFunctionInvocation;
226+
227+
public void RegisterFunctionInvocationHandler(Func<AutoFunctionInvocationContext, Func<AutoFunctionInvocationContext, Task>, Task> onFunctionInvocation)
228+
{
229+
this._onFunctionInvocation = onFunctionInvocation;
230+
}
231+
232+
public Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
233+
{
234+
if (this._onFunctionInvocation is null)
235+
{
236+
return next(context);
237+
}
238+
239+
return this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask;
240+
}
241+
}
242+
243+
#endregion
244+
}

0 commit comments

Comments
 (0)