Skip to content

Commit bde37f4

Browse files
authored
Merge branch 'main' into copilot/add-google-chat-client-service
2 parents 30f7107 + 4a09cbd commit bde37f4

26 files changed

Lines changed: 586 additions & 75 deletions

File tree

dotnet/Directory.Packages.props

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.Dapr" Version="9.9.0" />
1414
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.NodeJS.Extensions" Version="9.9.0" />
1515
<PackageVersion Include="Aspire.Hosting.Azure.Search" Version="13.0.0" />
16-
<PackageVersion Include="AWSSDK.BedrockAgent" Version="4.0.5.7" />
17-
<PackageVersion Include="AWSSDK.BedrockAgentRuntime" Version="4.0.6.3" />
18-
<PackageVersion Include="AWSSDK.BedrockRuntime" Version="4.0.7.2" />
19-
<PackageVersion Include="AWSSDK.Core" Version="4.0.0.32" />
20-
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.3.5" />
21-
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="4.0.3.4" />
22-
<PackageVersion Include="AWSSDK.SecurityToken" Version="4.0.2.6" />
16+
<PackageVersion Include="AWSSDK.BedrockAgent" Version="4.0.7.5" />
17+
<PackageVersion Include="AWSSDK.BedrockAgentRuntime" Version="4.0.8.5" />
18+
<PackageVersion Include="AWSSDK.BedrockRuntime" Version="4.0.14.5" />
19+
<PackageVersion Include="AWSSDK.Core" Version="4.0.3.8" />
20+
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5.3" />
21+
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="4.0.3.19" />
22+
<PackageVersion Include="AWSSDK.SecurityToken" Version="4.0.5.6" />
2323
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.0.0" />
2424
<PackageVersion Include="Azure.AI.ContentSafety" Version="1.0.0" />
2525
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
@@ -162,7 +162,7 @@
162162
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
163163
<PackageVersion Include="DuckDB.NET.Data.Full" Version="1.2.0" />
164164
<PackageVersion Include="DuckDB.NET.Data" Version="1.1.3" />
165-
<PackageVersion Include="MongoDB.Driver" Version="2.30.0" />
165+
<PackageVersion Include="MongoDB.Driver" Version="3.5.2" />
166166
<PackageVersion Include="Microsoft.Graph" Version="5.94.0" />
167167
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.24" />
168168
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Globalization;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Net.Http;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.AI;
11+
using Microsoft.SemanticKernel;
12+
using Microsoft.SemanticKernel.ChatCompletion;
13+
using Microsoft.SemanticKernel.Connectors.Google;
14+
using Xunit;
15+
16+
namespace SemanticKernel.Connectors.Google.UnitTests.Core.Gemini.Clients;
17+
18+
/// <summary>
19+
/// Unit tests for IChatClient-based function calling with Gemini using FunctionChoiceBehavior.
20+
/// </summary>
21+
public sealed class GeminiChatClientFunctionCallingTests : IDisposable
22+
{
23+
private readonly HttpClient _httpClient;
24+
private readonly string _responseContent;
25+
private readonly string _responseContentWithFunction;
26+
private readonly HttpMessageHandlerStub _messageHandlerStub;
27+
private readonly GeminiFunction _timePluginDate, _timePluginNow;
28+
private readonly Kernel _kernelWithFunctions;
29+
private const string ChatTestDataFilePath = "./TestData/chat_one_response.json";
30+
private const string ChatTestDataWithFunctionFilePath = "./TestData/chat_one_function_response.json";
31+
32+
public GeminiChatClientFunctionCallingTests()
33+
{
34+
this._responseContent = File.ReadAllText(ChatTestDataFilePath);
35+
this._responseContentWithFunction = File.ReadAllText(ChatTestDataWithFunctionFilePath)
36+
.Replace("%nameSeparator%", GeminiFunction.NameSeparator, StringComparison.Ordinal);
37+
this._messageHandlerStub = new HttpMessageHandlerStub();
38+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(
39+
this._responseContent);
40+
41+
this._httpClient = new HttpClient(this._messageHandlerStub, false);
42+
43+
var kernelPlugin = KernelPluginFactory.CreateFromFunctions("TimePlugin", new[]
44+
{
45+
KernelFunctionFactory.CreateFromMethod((string? format = null)
46+
=> DateTime.Now.Date.ToString(format, CultureInfo.InvariantCulture), "Date", "TimePlugin.Date"),
47+
KernelFunctionFactory.CreateFromMethod(()
48+
=> DateTime.Now.ToString("", CultureInfo.InvariantCulture), "Now", "TimePlugin.Now",
49+
parameters: [new KernelParameterMetadata("param1") { ParameterType = typeof(string), Description = "desc", IsRequired = false }]),
50+
});
51+
IList<KernelFunctionMetadata> functions = kernelPlugin.GetFunctionsMetadata();
52+
53+
this._timePluginDate = functions[0].ToGeminiFunction();
54+
this._timePluginNow = functions[1].ToGeminiFunction();
55+
56+
this._kernelWithFunctions = new Kernel();
57+
this._kernelWithFunctions.Plugins.Add(kernelPlugin);
58+
}
59+
60+
[Fact]
61+
public async Task ChatClientShouldConvertToIChatClientSuccessfullyAsync()
62+
{
63+
// Arrange
64+
var chatCompletionService = this.CreateChatCompletionService();
65+
66+
// Act
67+
var chatClient = chatCompletionService.AsChatClient();
68+
69+
// Assert - Verify conversion works
70+
Assert.NotNull(chatClient);
71+
Assert.IsAssignableFrom<IChatClient>(chatClient);
72+
73+
// Verify we can make a basic call through IChatClient
74+
var messages = new List<ChatMessage>
75+
{
76+
new(ChatRole.User, "What time is it?")
77+
};
78+
79+
var response = await chatClient.GetResponseAsync(messages);
80+
81+
Assert.NotNull(response);
82+
Assert.NotEmpty(response.Messages);
83+
}
84+
85+
[Fact]
86+
public async Task ChatClientShouldReceiveFunctionCallsInResponseAsync()
87+
{
88+
// Arrange
89+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(this._responseContentWithFunction);
90+
var chatCompletionService = this.CreateChatCompletionService();
91+
var chatClient = chatCompletionService.AsChatClient();
92+
93+
var settings = new GeminiPromptExecutionSettings
94+
{
95+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
96+
};
97+
var chatOptions = settings.ToChatOptions(this._kernelWithFunctions);
98+
99+
var messages = new List<ChatMessage>
100+
{
101+
new(ChatRole.User, "What time is it?")
102+
};
103+
104+
// Act
105+
var response = await chatClient.GetResponseAsync(messages, chatOptions);
106+
107+
// Assert - Verify that FunctionCallContent is returned in the response
108+
Assert.NotNull(response);
109+
var functionCalls = response.Messages
110+
.SelectMany(m => m.Contents)
111+
.OfType<Microsoft.Extensions.AI.FunctionCallContent>()
112+
.ToList();
113+
114+
Assert.NotEmpty(functionCalls);
115+
var functionCall = functionCalls.First();
116+
Assert.Contains(this._timePluginNow.FunctionName, functionCall.Name, StringComparison.OrdinalIgnoreCase);
117+
}
118+
119+
[Fact]
120+
public async Task ChatClientShouldStreamResponsesAsync()
121+
{
122+
// Arrange
123+
var chatCompletionService = this.CreateChatCompletionService();
124+
var chatClient = chatCompletionService.AsChatClient();
125+
126+
var settings = new GeminiPromptExecutionSettings
127+
{
128+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
129+
};
130+
var chatOptions = settings.ToChatOptions(this._kernelWithFunctions);
131+
132+
var messages = new List<ChatMessage>
133+
{
134+
new(ChatRole.User, "What time is it?")
135+
};
136+
137+
// Act
138+
var updates = new List<ChatResponseUpdate>();
139+
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, chatOptions))
140+
{
141+
updates.Add(update);
142+
}
143+
144+
// Assert - Verify that streaming works and returns updates
145+
Assert.NotEmpty(updates);
146+
}
147+
148+
[Fact]
149+
public async Task AsChatClientConvertsServiceToIChatClientAsync()
150+
{
151+
// Arrange
152+
var chatCompletionService = this.CreateChatCompletionService();
153+
154+
// Act
155+
var chatClient = chatCompletionService.AsChatClient();
156+
157+
// Assert
158+
Assert.NotNull(chatClient);
159+
Assert.IsAssignableFrom<IChatClient>(chatClient);
160+
}
161+
162+
private GoogleAIGeminiChatCompletionService CreateChatCompletionService(HttpClient? httpClient = null)
163+
{
164+
return new GoogleAIGeminiChatCompletionService(
165+
modelId: "fake-model",
166+
apiKey: "fake-key",
167+
apiVersion: GoogleAIVersion.V1,
168+
httpClient: httpClient ?? this._httpClient);
169+
}
170+
171+
public void Dispose()
172+
{
173+
this._httpClient.Dispose();
174+
this._messageHandlerStub.Dispose();
175+
}
176+
}

dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiToolCallBehaviorTests.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,111 @@ public void KernelFunctionsCloneReturnsCorrectClone()
194194
Assert.Equivalent(toolcallbehavior, clone, strict: true);
195195
}
196196

197+
[Fact]
198+
public void FunctionChoiceBehaviorAutoConvertsToAutoInvokeKernelFunctions()
199+
{
200+
// Arrange
201+
var settings = new GeminiPromptExecutionSettings
202+
{
203+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
204+
};
205+
206+
// Act
207+
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
208+
209+
// Assert
210+
Assert.NotNull(converted.ToolCallBehavior);
211+
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
212+
Assert.True(converted.ToolCallBehavior.MaximumAutoInvokeAttempts > 0);
213+
}
214+
215+
[Fact]
216+
public void FunctionChoiceBehaviorAutoWithNoAutoInvokeConvertsToEnableKernelFunctions()
217+
{
218+
// Arrange
219+
var settings = new GeminiPromptExecutionSettings
220+
{
221+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
222+
};
223+
224+
// Act
225+
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
226+
227+
// Assert
228+
Assert.NotNull(converted.ToolCallBehavior);
229+
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
230+
Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
231+
}
232+
233+
[Fact]
234+
public void FunctionChoiceBehaviorRequiredConvertsToAutoInvokeKernelFunctions()
235+
{
236+
// Arrange
237+
var settings = new GeminiPromptExecutionSettings
238+
{
239+
FunctionChoiceBehavior = FunctionChoiceBehavior.Required()
240+
};
241+
242+
// Act
243+
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
244+
245+
// Assert
246+
Assert.NotNull(converted.ToolCallBehavior);
247+
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
248+
Assert.True(converted.ToolCallBehavior.MaximumAutoInvokeAttempts > 0);
249+
}
250+
251+
[Fact]
252+
public void FunctionChoiceBehaviorNoneConvertsToEnableKernelFunctions()
253+
{
254+
// Arrange
255+
var settings = new GeminiPromptExecutionSettings
256+
{
257+
FunctionChoiceBehavior = FunctionChoiceBehavior.None()
258+
};
259+
260+
// Act
261+
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
262+
263+
// Assert
264+
Assert.NotNull(converted.ToolCallBehavior);
265+
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
266+
// None behavior doesn't auto-invoke
267+
Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
268+
}
269+
270+
[Fact]
271+
public void GeminiPromptExecutionSettingsWithNoFunctionChoiceBehaviorDoesNotSetToolCallBehavior()
272+
{
273+
// Arrange
274+
var settings = new GeminiPromptExecutionSettings();
275+
276+
// Act
277+
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
278+
279+
// Assert
280+
Assert.Null(converted.ToolCallBehavior);
281+
}
282+
283+
[Fact]
284+
public void GeminiPromptExecutionSettingsPreservesExistingToolCallBehavior()
285+
{
286+
// Arrange
287+
var settings = new GeminiPromptExecutionSettings
288+
{
289+
ToolCallBehavior = GeminiToolCallBehavior.EnableKernelFunctions,
290+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
291+
};
292+
293+
// Act
294+
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
295+
296+
// Assert - ToolCallBehavior should be preserved when already set
297+
Assert.NotNull(converted.ToolCallBehavior);
298+
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
299+
Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
300+
}
301+
197302
private static KernelPlugin GetTestPlugin()
198303
{
199304
var function = KernelFunctionFactory.CreateFromMethod(

0 commit comments

Comments
 (0)