Skip to content

Commit 41292cc

Browse files
committed
Introduce Grouding with Google Search configuration
1 parent e44bfb1 commit 41292cc

File tree

14 files changed

+396
-1
lines changed

14 files changed

+396
-1
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.SemanticKernel;
4+
using Microsoft.SemanticKernel.ChatCompletion;
5+
using Microsoft.SemanticKernel.Connectors.Google;
6+
7+
namespace ChatCompletion;
8+
9+
/// <summary>
10+
/// These examples demonstrate different ways of using chat completion with Google AI API.
11+
/// <para>
12+
/// Currently grounding with Google Search is only supported in Google AI Gemini 2.0+ models
13+
/// See: https://ai.google.dev/gemini-api/docs/grounding
14+
/// </para>
15+
/// </summary>
16+
public sealed class Google_GeminiChatCompletionGroudingWithGoogleSearch(ITestOutputHelper output) : BaseTest(output)
17+
{
18+
[Fact]
19+
public async Task GoogleAIChatCompletionUsingThinkingBudget()
20+
{
21+
Console.WriteLine("============= Google AI - Gemini 2.5 Chat Completion using Grounding with Google Search =============");
22+
23+
Assert.NotNull(TestConfiguration.GoogleAI.ApiKey);
24+
string geminiModelId = "gemini-2.5-pro-exp-03-25";
25+
26+
Kernel kernel = Kernel.CreateBuilder()
27+
.AddGoogleAIGeminiChatCompletion(
28+
modelId: geminiModelId,
29+
apiKey: TestConfiguration.GoogleAI.ApiKey)
30+
.Build();
31+
32+
var chatHistory = new ChatHistory("You are an expert in the tool shop.");
33+
var chat = kernel.GetRequiredService<IChatCompletionService>();
34+
var executionSettings = new GeminiPromptExecutionSettings
35+
{
36+
// This parameter enables grouding with google search.
37+
GroundingConfig = new() { GoogleSearch = new() }
38+
};
39+
40+
// First user message
41+
chatHistory.AddUserMessage("Hi, I'm looking for new power tools, any suggestion?");
42+
await MessageOutputAsync(chatHistory);
43+
44+
// First assistant message
45+
var reply = await chat.GetChatMessageContentAsync(chatHistory, executionSettings);
46+
chatHistory.Add(reply);
47+
await MessageOutputAsync(chatHistory);
48+
}
49+
50+
/// <summary>
51+
/// Outputs the last message of the chat history
52+
/// </summary>
53+
private Task MessageOutputAsync(ChatHistory chatHistory)
54+
{
55+
var message = chatHistory.Last();
56+
57+
Console.WriteLine($"{message.Role}: {message.Content}");
58+
Console.WriteLine("------------------------");
59+
60+
return Task.CompletedTask;
61+
}
62+
}

dotnet/samples/Concepts/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom
6464
- [Google_GeminiChatCompletion](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletion.cs)
6565
- [Google_GeminiChatCompletionStreaming](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionStreaming.cs)
6666
- [Google_GeminiChatCompletionWithThinkingBudget](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs)
67+
- [Google_GeminiChatCompletionGroundingWithGoogleSearch](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionGroudingWithGoogleSearch.cs)
6768
- [Google_GeminiGetModelResult](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiGetModelResult.cs)
6869
- [Google_GeminiStructuredOutputs](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiStructuredOutputs.cs)
6970
- [Google_GeminiVision](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiVision.cs)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ public void ItCreatesGeminiExecutionSettingsFromJsonSnakeCase()
112112
],
113113
"thinking_config": {
114114
"thinking_budget": 1000
115+
},
116+
"grounding_config": {
117+
"google_search": {}
115118
}
116119
}
117120
""";
@@ -134,6 +137,7 @@ public void ItCreatesGeminiExecutionSettingsFromJsonSnakeCase()
134137
settings.Threshold.Equals(threshold));
135138

136139
Assert.Equal(1000, executionSettings.ThinkingConfig?.ThinkingBudget);
140+
Assert.NotNull(executionSettings.GroundingConfig?.GoogleSearch);
137141
}
138142

139143
[Fact]
@@ -160,6 +164,9 @@ public void PromptExecutionSettingsCloneWorksAsExpected()
160164
],
161165
"thinking_config": {
162166
"thinking_budget": 1000
167+
},
168+
"grouding_config": {
169+
"google_search": {}
163170
}
164171
}
165172
""";
@@ -177,6 +184,7 @@ public void PromptExecutionSettingsCloneWorksAsExpected()
177184
Assert.Equivalent(executionSettings.SafetySettings, clone.SafetySettings);
178185
Assert.Equal(executionSettings.AudioTimestamp, clone.AudioTimestamp);
179186
Assert.Equivalent(executionSettings.ThinkingConfig, clone.ThinkingConfig);
187+
Assert.Equivalent(executionSettings.GroundingConfig, clone.GroundingConfig);
180188
}
181189

182190
[Fact]
@@ -220,5 +228,6 @@ public void PromptExecutionSettingsFreezeWorksAsExpected()
220228
Assert.Throws<NotSupportedException>(() => executionSettings.StopSequences!.Add("baz"));
221229
Assert.Throws<NotSupportedException>(() => executionSettings.SafetySettings!.Add(new GeminiSafetySetting(GeminiSafetyCategory.Toxicity, GeminiSafetyThreshold.Unspecified)));
222230
Assert.Throws<InvalidOperationException>(() => executionSettings.ThinkingConfig = new GeminiThinkingConfig { ThinkingBudget = 1 });
231+
Assert.Throws<InvalidOperationException>(() => executionSettings.GroundingConfig = new GeminiGroundingConfig { GoogleSearch = new object() });
223232
}
224233
}

dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,41 @@ public async Task RequestBodyIncludesThinkingConfigWhenSetAsync(int? thinkingBud
144144
}
145145
}
146146

147+
[Theory]
148+
[InlineData(false)]
149+
[InlineData(true)]
150+
public async Task RequestBodyIncludesGoogleSearchToolWhenSetAsync(bool shouldContain)
151+
{
152+
// Arrange
153+
string model = "gemini-2.5-pro";
154+
var sut = new GoogleAIGeminiChatCompletionService(model, "key", httpClient: this._httpClient);
155+
156+
var executionSettings = new GeminiPromptExecutionSettings
157+
{
158+
GroundingConfig = shouldContain
159+
? new GeminiGroundingConfig() { GoogleSearch = new() }
160+
: null
161+
};
162+
163+
// Act
164+
var result = await sut.GetChatMessageContentAsync("my prompt", executionSettings);
165+
166+
// Assert
167+
Assert.NotNull(result);
168+
Assert.NotNull(this._messageHandlerStub.RequestContent);
169+
170+
var requestBody = UTF8Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent);
171+
172+
if (shouldContain)
173+
{
174+
Assert.Contains("googleSearch", requestBody);
175+
}
176+
else
177+
{
178+
Assert.DoesNotContain("googleSearch", requestBody);
179+
}
180+
}
181+
147182
public void Dispose()
148183
{
149184
this._httpClient.Dispose();

dotnet/src/Connectors/Connectors.Google.UnitTests/Services/VertexAIGeminiChatCompletionServiceTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,41 @@ public async Task RequestBodyIncludesThinkingConfigWhenSetAsync(int? thinkingBud
156156
}
157157
}
158158

159+
[Theory]
160+
[InlineData(false)]
161+
[InlineData(true)]
162+
public async Task RequestBodyIncludesGoogleSearchToolWhenSetAsync(bool shouldContain)
163+
{
164+
// Arrange
165+
string model = "gemini-2.5-pro";
166+
var sut = new VertexAIGeminiChatCompletionService(model, () => new ValueTask<string>("key"), "location", "project", httpClient: this._httpClient);
167+
168+
var executionSettings = new GeminiPromptExecutionSettings
169+
{
170+
GroundingConfig = shouldContain
171+
? new GeminiGroundingConfig() { GoogleSearch = new() }
172+
: null
173+
};
174+
175+
// Act
176+
var result = await sut.GetChatMessageContentAsync("my prompt", executionSettings);
177+
178+
// Assert
179+
Assert.NotNull(result);
180+
Assert.NotNull(this._messageHandlerStub.RequestContent);
181+
182+
var requestBody = UTF8Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent);
183+
184+
if (shouldContain)
185+
{
186+
Assert.Contains("googleSearch", requestBody);
187+
}
188+
else
189+
{
190+
Assert.DoesNotContain("googleSearch", requestBody);
191+
}
192+
}
193+
159194
public void Dispose()
160195
{
161196
this._httpClient.Dispose();

dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@ private static GeminiMetadata GetResponseMetadata(
691691
PromptFeedbackBlockReason = geminiResponse.PromptFeedback?.BlockReason,
692692
PromptFeedbackSafetyRatings = geminiResponse.PromptFeedback?.SafetyRatings.ToList(),
693693
ResponseSafetyRatings = candidate.SafetyRatings?.ToList(),
694+
GroundingMetadata = candidate.GroundingMetadata
694695
};
695696

696697
private sealed class ChatCompletionState

dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,16 @@ private static void AddAdditionalBodyFields(GeminiPromptExecutionSettings execut
461461
request.Configuration ??= new ConfigurationElement();
462462
request.Configuration.ThinkingConfig = new GeminiRequestThinkingConfig { ThinkingBudget = executionSettings.ThinkingConfig.ThinkingBudget };
463463
}
464+
465+
if (executionSettings.GroundingConfig?.GoogleSearch is not null)
466+
{
467+
request.Tools ??= [];
468+
if (request.Tools.Count == 0)
469+
{
470+
request.Tools.Add(new GeminiTool());
471+
}
472+
request.Tools[0].GoogleSearch = new GeminiTool.GoogleSearchProperties();
473+
}
464474
}
465475

466476
internal sealed class ConfigurationElement

dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiResponseCandidate.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,13 @@ internal sealed class GeminiResponseCandidate
4545
/// </summary>
4646
[JsonPropertyName("tokenCount")]
4747
public int TokenCount { get; set; }
48+
49+
/// <summary>
50+
/// Optional. GroundingMetadata of response candidate.
51+
/// </summary>
52+
/// <remarks>
53+
/// Returned when Grounding with Google Search is enabled.
54+
/// </remarks>
55+
[JsonPropertyName("groundingMetadata")]
56+
public GeminiGroundingMetadata? GroundingMetadata { get; set; }
4857
}

dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiTool.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ internal sealed class GeminiTool
2525
[JsonPropertyName("functionDeclarations")]
2626
public IList<FunctionDeclaration> Functions { get; set; } = [];
2727

28+
/// <summary>
29+
/// Optional. Google Search configuration that allows the model to use Google Search as a tool to improve response quality.
30+
/// When this property is set (even with empty object), the model can decide to use Google Search autonomously.
31+
/// </summary>
32+
/// <remarks>
33+
/// The Search-as-a-tool functionality also enables multi-turn searches. Combining Search with function calling is not yet supported.
34+
/// </remarks>
35+
[JsonPropertyName("googleSearch")]
36+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
37+
public GoogleSearchProperties? GoogleSearch { get; set; }
38+
2839
/// <summary>
2940
/// Structured representation of a function declaration as defined by the OpenAPI 3.03 specification.
3041
/// Included in this declaration are the function name and parameters.
@@ -56,4 +67,12 @@ internal sealed class FunctionDeclaration
5667
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
5768
public JsonElement? Parameters { get; set; }
5869
}
70+
71+
/// <summary>
72+
/// Configuration properties for Google Search integration.
73+
/// </summary>
74+
/// <remarks>
75+
/// An empty object is sufficient to enable Search as a tool.
76+
/// </remarks>
77+
internal sealed class GoogleSearchProperties;
5978
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json.Serialization;
4+
5+
namespace Microsoft.SemanticKernel.Connectors.Google;
6+
7+
/// <summary>
8+
/// GeminiGroundingConfig class
9+
/// </summary>
10+
public class GeminiGroundingConfig
11+
{
12+
/// <summary>The Grounding with Google Search feature in the Gemini API can be used to improve the accuracy and recency of responses from the model.</summary>
13+
/// <remarks>
14+
/// The model can decide when to use Google Search.
15+
/// This parameter is valid from Gemini 2.0+.
16+
/// Combining Search with function calling is not yet supported. Functions will be ignored when this parameter is set.
17+
/// </remarks>
18+
[JsonPropertyName("google_search")]
19+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
20+
public object? GoogleSearch { get; set; }
21+
22+
/// <summary>
23+
/// Clones this instance.
24+
/// </summary>
25+
/// <returns></returns>
26+
public GeminiGroundingConfig Clone()
27+
{
28+
return (GeminiGroundingConfig)this.MemberwiseClone();
29+
}
30+
}

0 commit comments

Comments
 (0)