Skip to content

Commit 6667c46

Browse files
authored
Merge branch 'main' into main
2 parents 666cb61 + 9118743 commit 6667c46

File tree

39 files changed

+3048
-131
lines changed

39 files changed

+3048
-131
lines changed

dotnet/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<PackageVersion Include="FastBertTokenizer" Version="1.0.28" />
3636
<PackageVersion Include="Google.Apis.Auth" Version="1.73.0" />
3737
<PackageVersion Include="Google.Apis.CustomSearchAPI.v1" Version="1.68.0.3520" />
38+
<PackageVersion Include="Google.GenAI" Version="0.11.0" />
3839
<PackageVersion Include="Google.Protobuf" Version="3.33.4" />
3940
<PackageVersion Include="Grpc.AspNetCore" Version="2.76.0" />
4041
<PackageVersion Include="Grpc.AspNetCore.Server" Version="2.70.0" />

dotnet/nuget/nuget-package.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
<Project>
22
<PropertyGroup>
33
<!-- Central version prefix - applies to all nuget packages. -->
4-
<VersionPrefix>1.68.0</VersionPrefix>
4+
<VersionPrefix>1.69.0</VersionPrefix>
55
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix)</PackageVersion>
66
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)</PackageVersion>
77

88
<Configurations>Debug;Release;Publish</Configurations>
99
<IsPackable>true</IsPackable>
1010

1111
<!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
12-
<PackageValidationBaselineVersion>1.67.1</PackageValidationBaselineVersion>
12+
<PackageValidationBaselineVersion>1.68.0</PackageValidationBaselineVersion>
1313
<!-- Validate assembly attributes only for Publish builds -->
1414
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
1515
<!-- Do not validate reference assemblies -->

dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
</PackageReference>
2626
<PackageReference Include="System.Numerics.Tensors" />
2727
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
28+
<PackageReference Include="Google.GenAI" />
2829
</ItemGroup>
2930

3031
<ItemGroup>

dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,83 @@ public async Task IfAutoInvokeShouldAllowFilterToModifyFunctionResultAsync()
626626
Assert.Contains(ModifiedResult, secondRequestContent);
627627
}
628628

629+
[Fact]
630+
public async Task FunctionCallWithThoughtSignatureIsCapturedInToolCallAsync()
631+
{
632+
// Arrange
633+
var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_function_with_thought_signature_response.json")
634+
.Replace("%nameSeparator%", GeminiFunction.NameSeparator, StringComparison.Ordinal);
635+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseWithThoughtSignature);
636+
637+
var client = this.CreateChatCompletionClient();
638+
var chatHistory = CreateSampleChatHistory();
639+
var executionSettings = new GeminiPromptExecutionSettings
640+
{
641+
ToolCallBehavior = GeminiToolCallBehavior.EnableFunctions([this._timePluginNow])
642+
};
643+
644+
// Act
645+
var messages = await client.GenerateChatMessageAsync(chatHistory, executionSettings: executionSettings, kernel: this._kernelWithFunctions);
646+
647+
// Assert
648+
Assert.Single(messages);
649+
var geminiMessage = messages[0] as GeminiChatMessageContent;
650+
Assert.NotNull(geminiMessage);
651+
Assert.NotNull(geminiMessage.ToolCalls);
652+
Assert.Single(geminiMessage.ToolCalls);
653+
Assert.Equal("test-thought-signature-abc123", geminiMessage.ToolCalls[0].ThoughtSignature);
654+
}
655+
656+
[Fact]
657+
public async Task TextResponseWithThoughtSignatureIsCapturedInMetadataAsync()
658+
{
659+
// Arrange
660+
var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_text_with_thought_signature_response.json");
661+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseWithThoughtSignature);
662+
663+
var client = this.CreateChatCompletionClient();
664+
var chatHistory = CreateSampleChatHistory();
665+
666+
// Act
667+
var messages = await client.GenerateChatMessageAsync(chatHistory);
668+
669+
// Assert
670+
Assert.Single(messages);
671+
var geminiMessage = messages[0] as GeminiChatMessageContent;
672+
Assert.NotNull(geminiMessage);
673+
Assert.NotNull(geminiMessage.Metadata);
674+
var metadata = geminiMessage.Metadata as GeminiMetadata;
675+
Assert.NotNull(metadata);
676+
Assert.Equal("text-response-thought-signature-xyz789", metadata.ThoughtSignature);
677+
}
678+
679+
[Fact]
680+
public async Task ThoughtSignatureIsIncludedInSubsequentRequestAsync()
681+
{
682+
// Arrange - First response has function call with ThoughtSignature
683+
var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_function_with_thought_signature_response.json")
684+
.Replace("%nameSeparator%", GeminiFunction.NameSeparator, StringComparison.Ordinal);
685+
using var handlerStub = new MultipleHttpMessageHandlerStub();
686+
handlerStub.AddJsonResponse(responseWithThoughtSignature);
687+
handlerStub.AddJsonResponse(this._responseContent); // Second response is text
688+
689+
using var httpClient = new HttpClient(handlerStub, false);
690+
var client = this.CreateChatCompletionClient(httpClient: httpClient);
691+
var chatHistory = CreateSampleChatHistory();
692+
var executionSettings = new GeminiPromptExecutionSettings
693+
{
694+
ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
695+
};
696+
697+
// Act
698+
await client.GenerateChatMessageAsync(chatHistory, executionSettings: executionSettings, kernel: this._kernelWithFunctions);
699+
700+
// Assert - Check that the second request includes the ThoughtSignature
701+
var secondRequestContent = handlerStub.GetRequestContentAsString(1);
702+
Assert.NotNull(secondRequestContent);
703+
Assert.Contains("test-thought-signature-abc123", secondRequestContent);
704+
}
705+
629706
private static ChatHistory CreateSampleChatHistory()
630707
{
631708
var chatHistory = new ChatHistory();

dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,63 @@ await client.StreamGenerateChatMessageAsync(chatHistory, executionSettings: exec
417417
c is GeminiChatMessageContent gm && gm.Role == AuthorRole.Tool && gm.CalledToolResult is not null);
418418
}
419419

420+
[Fact]
421+
public async Task StreamingTextResponseWithAutoInvokeAndEmptyToolCallsDoesNotEnterToolCallingBranchAsync()
422+
{
423+
// Arrange - This tests the Phase 6 bug fix: empty ToolCalls list should not trigger tool calling
424+
var client = this.CreateChatCompletionClient();
425+
var chatHistory = CreateSampleChatHistory();
426+
var executionSettings = new GeminiPromptExecutionSettings
427+
{
428+
ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
429+
};
430+
431+
// Response is text-only (no function calls), so ToolCalls will be empty list
432+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(this._responseContent);
433+
434+
// Act
435+
var messages = await client.StreamGenerateChatMessageAsync(
436+
chatHistory,
437+
executionSettings: executionSettings,
438+
kernel: this._kernelWithFunctions).ToListAsync();
439+
440+
// Assert - Should yield text response without entering tool-calling branch
441+
Assert.NotEmpty(messages);
442+
Assert.All(messages, m =>
443+
{
444+
var geminiMessage = m as GeminiStreamingChatMessageContent;
445+
Assert.NotNull(geminiMessage);
446+
// ToolCalls should be null or empty for text responses
447+
Assert.True(geminiMessage.ToolCalls is null || geminiMessage.ToolCalls.Count == 0);
448+
});
449+
}
450+
451+
[Fact]
452+
public async Task StreamingTextResponseWithAutoInvokeAndNullToolCallsDoesNotEnterToolCallingBranchAsync()
453+
{
454+
// Arrange - This tests that pattern `is { Count: > 0 }` handles null safely
455+
var client = this.CreateChatCompletionClient();
456+
var chatHistory = CreateSampleChatHistory();
457+
var executionSettings = new GeminiPromptExecutionSettings
458+
{
459+
ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
460+
};
461+
462+
// Response is text-only
463+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(this._responseContent);
464+
465+
// Act
466+
var messages = await client.StreamGenerateChatMessageAsync(
467+
chatHistory,
468+
executionSettings: executionSettings,
469+
kernel: this._kernelWithFunctions).ToListAsync();
470+
471+
// Assert - Should complete without errors
472+
Assert.NotEmpty(messages);
473+
// Verify we got text content
474+
Assert.Contains(messages, m => !string.IsNullOrEmpty(m.Content));
475+
}
476+
420477
private static ChatHistory CreateSampleChatHistory()
421478
{
422479
var chatHistory = new ChatHistory();

dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionToolCallTests.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,71 @@ public void ToStringReturnsCorrectValue()
6868
// Act & Assert
6969
Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", functionToolCall.ToString());
7070
}
71+
72+
[Fact]
73+
public void ThoughtSignatureIsNullWhenCreatedFromFunctionCallPart()
74+
{
75+
// Arrange - Using the FunctionCallPart constructor (no ThoughtSignature)
76+
var toolCallPart = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" };
77+
var functionToolCall = new GeminiFunctionToolCall(toolCallPart);
78+
79+
// Act & Assert
80+
Assert.Null(functionToolCall.ThoughtSignature);
81+
}
82+
83+
[Fact]
84+
public void ThoughtSignatureIsCapturedWhenCreatedFromGeminiPart()
85+
{
86+
// Arrange - Using the GeminiPart constructor (with ThoughtSignature)
87+
var part = new GeminiPart
88+
{
89+
FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" },
90+
ThoughtSignature = "test-thought-signature-123"
91+
};
92+
var functionToolCall = new GeminiFunctionToolCall(part);
93+
94+
// Act & Assert
95+
Assert.Equal("test-thought-signature-123", functionToolCall.ThoughtSignature);
96+
}
97+
98+
[Fact]
99+
public void ThoughtSignatureIsNullWhenGeminiPartHasNoSignature()
100+
{
101+
// Arrange
102+
var part = new GeminiPart
103+
{
104+
FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" },
105+
ThoughtSignature = null
106+
};
107+
var functionToolCall = new GeminiFunctionToolCall(part);
108+
109+
// Act & Assert
110+
Assert.Null(functionToolCall.ThoughtSignature);
111+
}
112+
113+
[Fact]
114+
public void ArgumentsArePreservedWhenCreatedFromGeminiPart()
115+
{
116+
// Arrange
117+
var part = new GeminiPart
118+
{
119+
FunctionCall = new GeminiPart.FunctionCallPart
120+
{
121+
FunctionName = "MyPlugin_MyFunction",
122+
Arguments = new JsonObject
123+
{
124+
{ "location", "San Diego" },
125+
{ "max_price", 300 }
126+
}
127+
},
128+
ThoughtSignature = "signature-abc"
129+
};
130+
var functionToolCall = new GeminiFunctionToolCall(part);
131+
132+
// Act & Assert
133+
Assert.NotNull(functionToolCall.Arguments);
134+
Assert.Equal(2, functionToolCall.Arguments.Count);
135+
Assert.Equal("San Diego", functionToolCall.Arguments["location"]!.ToString());
136+
Assert.Equal("signature-abc", functionToolCall.ThoughtSignature);
137+
}
71138
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.SemanticKernel.Connectors.Google;
4+
using Xunit;
5+
6+
namespace SemanticKernel.Connectors.Google.UnitTests.Core.Gemini;
7+
8+
/// <summary>
9+
/// Unit tests for <see cref="GeminiMetadata"/> class.
10+
/// </summary>
11+
public sealed class GeminiMetadataTests
12+
{
13+
[Fact]
14+
public void ThoughtSignatureCanBeSetAndRetrieved()
15+
{
16+
// Arrange & Act
17+
var metadata = new GeminiMetadata { ThoughtSignature = "test-signature-123" };
18+
19+
// Assert
20+
Assert.Equal("test-signature-123", metadata.ThoughtSignature);
21+
}
22+
23+
[Fact]
24+
public void ThoughtSignatureIsNullByDefault()
25+
{
26+
// Arrange & Act
27+
var metadata = new GeminiMetadata();
28+
29+
// Assert
30+
Assert.Null(metadata.ThoughtSignature);
31+
}
32+
33+
[Fact]
34+
public void ThoughtSignatureIsStoredInDictionary()
35+
{
36+
// Arrange
37+
var metadata = new GeminiMetadata { ThoughtSignature = "dict-signature" };
38+
39+
// Act
40+
var hasKey = metadata.TryGetValue("ThoughtSignature", out var value);
41+
42+
// Assert
43+
Assert.True(hasKey);
44+
Assert.Equal("dict-signature", value);
45+
}
46+
47+
[Fact]
48+
public void ThoughtSignatureCanBeRetrievedFromDictionary()
49+
{
50+
// Arrange - This simulates deserialized metadata
51+
var metadata = new GeminiMetadata { ThoughtSignature = "from-dict" };
52+
53+
// Act
54+
var signature = metadata.ThoughtSignature;
55+
56+
// Assert
57+
Assert.Equal("from-dict", signature);
58+
}
59+
}

0 commit comments

Comments
 (0)