Skip to content

Commit 42716d5

Browse files
roldengarmrogerbarretomarkwallace-microsoft
authored
Add IncludeThoughts parameter to Google Connector for accessing Gemini reasoning content (#13383)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> This implements the IncludeThoughts parameter for the Google connector. See [related issue here](#13360). ### Description Adds `IncludeThoughts` parameter to `GeminiThinkingConfig` to enable access to Gemini's reasoning process. When enabled, thinking content is exposed via the `Items` collection as `ReasoningContent` objects, allowing developers to access both the model's internal reasoning and final response separately. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [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) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄 --------- Co-authored-by: Roger Barreto <[email protected]> Co-authored-by: Mark Wallace <[email protected]>
1 parent b50299b commit 42716d5

File tree

7 files changed

+342
-14
lines changed

7 files changed

+342
-14
lines changed

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

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Text.Json;
1010
using System.Threading.Tasks;
1111
using Microsoft.Extensions.Logging;
12+
using Microsoft.SemanticKernel;
1213
using Microsoft.SemanticKernel.ChatCompletion;
1314
using Microsoft.SemanticKernel.Connectors.Google;
1415
using Microsoft.SemanticKernel.Connectors.Google.Core;
@@ -597,6 +598,155 @@ private sealed class BearerTokenGenerator()
597598
public ValueTask<string> GetBearerToken() => ValueTask.FromResult(this.BearerKeys[this._index++]);
598599
}
599600

601+
[Fact]
602+
public async Task ShouldHandleThoughtAndTextPartsAsync()
603+
{
604+
// Arrange
605+
var responseContent = """
606+
{
607+
"candidates": [
608+
{
609+
"content": {
610+
"parts": [
611+
{
612+
"text": "Let me think about this...",
613+
"thought": true
614+
},
615+
{
616+
"text": "The answer is 42."
617+
}
618+
],
619+
"role": "model"
620+
},
621+
"finishReason": "STOP",
622+
"index": 0
623+
}
624+
]
625+
}
626+
""";
627+
628+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseContent);
629+
var client = this.CreateChatCompletionClient();
630+
var chatHistory = CreateSampleChatHistory();
631+
632+
// Act
633+
var result = await client.GenerateChatMessageAsync(chatHistory);
634+
635+
// Assert
636+
Assert.NotNull(result);
637+
var message = result[0];
638+
Assert.Equal("The answer is 42.", message.Content);
639+
Assert.Equal(2, message.Items.Count);
640+
641+
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
642+
var reasoningContent = message.Items.OfType<ReasoningContent>().FirstOrDefault();
643+
Assert.NotNull(reasoningContent);
644+
Assert.Equal("Let me think about this...", reasoningContent.Text);
645+
646+
var textContent = message.Items.OfType<TextContent>().FirstOrDefault();
647+
Assert.NotNull(textContent);
648+
Assert.Equal("The answer is 42.", textContent.Text);
649+
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
650+
}
651+
652+
[Fact]
653+
public async Task ShouldHandleOnlyThoughtPartsAsync()
654+
{
655+
// Arrange
656+
var responseContent = """
657+
{
658+
"candidates": [
659+
{
660+
"content": {
661+
"parts": [
662+
{
663+
"text": "This is just thinking content...",
664+
"thought": true
665+
}
666+
],
667+
"role": "model"
668+
},
669+
"finishReason": "STOP",
670+
"index": 0
671+
}
672+
]
673+
}
674+
""";
675+
676+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseContent);
677+
var client = this.CreateChatCompletionClient();
678+
var chatHistory = CreateSampleChatHistory();
679+
680+
// Act
681+
var result = await client.GenerateChatMessageAsync(chatHistory);
682+
683+
// Assert
684+
Assert.NotNull(result);
685+
var message = result[0];
686+
Assert.True(string.IsNullOrEmpty(message.Content));
687+
Assert.Single(message.Items);
688+
689+
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
690+
var reasoningContent = message.Items.OfType<ReasoningContent>().Single();
691+
Assert.Equal("This is just thinking content...", reasoningContent.Text);
692+
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
693+
}
694+
695+
[Fact]
696+
public async Task ShouldHandleMultipleThoughtPartsAsync()
697+
{
698+
// Arrange
699+
var responseContent = """
700+
{
701+
"candidates": [
702+
{
703+
"content": {
704+
"parts": [
705+
{
706+
"text": "First thought...",
707+
"thought": true
708+
},
709+
{
710+
"text": "Second thought...",
711+
"thought": true
712+
},
713+
{
714+
"text": "Final answer."
715+
}
716+
],
717+
"role": "model"
718+
},
719+
"finishReason": "STOP",
720+
"index": 0
721+
}
722+
]
723+
}
724+
""";
725+
726+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseContent);
727+
var client = this.CreateChatCompletionClient();
728+
var chatHistory = CreateSampleChatHistory();
729+
730+
// Act
731+
var result = await client.GenerateChatMessageAsync(chatHistory);
732+
733+
// Assert
734+
Assert.NotNull(result);
735+
var message = result[0];
736+
Assert.Equal("Final answer.", message.Content);
737+
Assert.Equal(3, message.Items.Count);
738+
739+
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
740+
var reasoningContents = message.Items.OfType<ReasoningContent>().ToList();
741+
Assert.Equal(2, reasoningContents.Count);
742+
Assert.Equal("First thought...", reasoningContents[0].Text);
743+
Assert.Equal("Second thought...", reasoningContents[1].Text);
744+
745+
var textContent = message.Items.OfType<TextContent>().Single();
746+
Assert.Equal("Final answer.", textContent.Text);
747+
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
748+
}
749+
600750
private static ChatHistory CreateSampleChatHistory()
601751
{
602752
var chatHistory = new ChatHistory();

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Net.Http;
88
using System.Text.Json;
99
using System.Threading.Tasks;
10+
using Microsoft.SemanticKernel;
1011
using Microsoft.SemanticKernel.ChatCompletion;
1112
using Microsoft.SemanticKernel.Connectors.Google;
1213
using Microsoft.SemanticKernel.Connectors.Google.Core;
@@ -428,6 +429,46 @@ public async Task ItCreatesPostRequestWithoutApiKeyInUrlAsync()
428429
Assert.DoesNotContain("key=", this._messageHandlerStub.RequestUri.ToString());
429430
}
430431

432+
[Fact]
433+
public async Task ShouldHandleStreamingThoughtPartsAsync()
434+
{
435+
// Arrange
436+
var streamingResponse = """
437+
data: {"candidates": [{"content": {"parts": [{"text": "Let me think...", "thought": true}], "role": "model"}, "index": 0}]}
438+
439+
data: {"candidates": [{"content": {"parts": [{"text": "The answer is"}], "role": "model"}, "index": 0}]}
440+
441+
data: {"candidates": [{"content": {"parts": [{"text": " 42."}], "role": "model"}, "finishReason": "STOP", "index": 0}]}
442+
443+
""";
444+
445+
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(streamingResponse);
446+
var client = this.CreateChatCompletionClient();
447+
var chatHistory = CreateSampleChatHistory();
448+
449+
// Act
450+
var messages = await client.StreamGenerateChatMessageAsync(chatHistory).ToListAsync();
451+
452+
// Assert
453+
Assert.Equal(3, messages.Count);
454+
455+
// First message should contain thought
456+
var firstMessage = messages[0];
457+
Assert.True(string.IsNullOrEmpty(firstMessage.Content));
458+
Assert.Single(firstMessage.Items);
459+
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
460+
var thoughtItem = firstMessage.Items.OfType<StreamingReasoningContent>().Single();
461+
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
462+
Assert.Equal("Let me think...", thoughtItem.InnerContent);
463+
464+
// Second and third messages contain regular text
465+
var secondMessage = messages[1];
466+
Assert.Equal("The answer is", secondMessage.Content);
467+
468+
var thirdMessage = messages[2];
469+
Assert.Equal(" 42.", thirdMessage.Content);
470+
}
471+
431472
private static ChatHistory CreateSampleChatHistory()
432473
{
433474
var chatHistory = new ChatHistory();

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,4 +268,67 @@ public void PromptExecutionSettingsFreezeWorksAsExpected()
268268
Assert.Throws<NotSupportedException>(() => executionSettings.SafetySettings!.Add(new GeminiSafetySetting(GeminiSafetyCategory.Toxicity, GeminiSafetyThreshold.Unspecified)));
269269
Assert.Throws<InvalidOperationException>(() => executionSettings.ThinkingConfig = new GeminiThinkingConfig { ThinkingBudget = 1 });
270270
}
271+
272+
[Fact]
273+
public void ItCreatesThinkingConfigWithIncludeThoughts()
274+
{
275+
// Arrange & Act
276+
var thinkingConfig = new GeminiThinkingConfig
277+
{
278+
ThinkingBudget = 2000,
279+
IncludeThoughts = true
280+
};
281+
282+
var executionSettings = new GeminiPromptExecutionSettings
283+
{
284+
ThinkingConfig = thinkingConfig
285+
};
286+
287+
// Assert
288+
Assert.NotNull(executionSettings.ThinkingConfig);
289+
Assert.Equal(2000, executionSettings.ThinkingConfig.ThinkingBudget);
290+
Assert.True(executionSettings.ThinkingConfig.IncludeThoughts);
291+
}
292+
293+
[Fact]
294+
public void ItSerializesThinkingConfigWithIncludeThoughts()
295+
{
296+
// Arrange
297+
var executionSettings = new GeminiPromptExecutionSettings
298+
{
299+
ThinkingConfig = new GeminiThinkingConfig
300+
{
301+
ThinkingBudget = 1500,
302+
IncludeThoughts = true
303+
}
304+
};
305+
306+
// Act
307+
var json = JsonSerializer.Serialize(executionSettings);
308+
309+
// Assert
310+
Assert.Contains("thinking_budget", json);
311+
Assert.Contains("1500", json);
312+
Assert.Contains("include_thoughts", json);
313+
Assert.Contains("true", json);
314+
}
315+
316+
[Fact]
317+
public void ItClonesThinkingConfigWithIncludeThoughts()
318+
{
319+
// Arrange
320+
var original = new GeminiThinkingConfig
321+
{
322+
ThinkingBudget = 3000,
323+
IncludeThoughts = true
324+
};
325+
326+
// Act
327+
var cloned = original.Clone();
328+
329+
// Assert
330+
Assert.NotSame(original, cloned);
331+
Assert.Equal(original.ThinkingBudget, cloned.ThinkingBudget);
332+
Assert.Equal(original.IncludeThoughts, cloned.IncludeThoughts);
333+
}
271334
}

0 commit comments

Comments
 (0)