Skip to content

Commit 8653be3

Browse files
authored
Release 1.9.3
* Add Gemini grounding support and config options Introduces Google Search grounding capabilities to GeminiHelper, including new config options for enabling grounding, setting thresholds, and choosing grounding mode. Updates IGeminiHelper interface and appsettings.json to support these features. Also refactors ImagenHelper for improved type safety. * Bump version to 1.9.3 in CommonUtilities.csproj Updated AssemblyVersion and FileVersion to 1.9.3 to reflect new release. No other changes made.
1 parent 23f9411 commit 8653be3

File tree

6 files changed

+283
-14
lines changed

6 files changed

+283
-14
lines changed

CommonUtilities/CommonUtilities.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
<Description>A modular, production-ready C#/.NET utility library and toolkit for rapid development.</Description>
1414
<RepositoryUrl>https://github.com/LoveDoLove/CS_CommonUtilities</RepositoryUrl>
1515
<RepositoryType>git</RepositoryType>
16-
<AssemblyVersion>1.9.2</AssemblyVersion>
17-
<FileVersion>1.9.2</FileVersion>
16+
<AssemblyVersion>1.9.3</AssemblyVersion>
17+
<FileVersion>1.9.3</FileVersion>
1818
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1919
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
2020
</PropertyGroup>

CommonUtilities/Helpers/GoogleAI/GeminiConfig.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class GeminiConfig
3030
/// <summary>
3131
/// Model name (e.g., "models/gemini-1.5-flash", "models/gemini-2.0-flash-exp").
3232
/// </summary>
33-
public string Model { get; set; } = "models/gemini-1.5-flash";
33+
public string Model { get; set; } = "models/gemini-2.5-flash-lite";
3434

3535
/// <summary>
3636
/// Optional: Project ID for Vertex AI.
@@ -53,4 +53,26 @@ public class GeminiConfig
5353
/// </summary>
5454
[DefaultValue(false)]
5555
public bool ExpressMode { get; set; } = false;
56+
57+
/// <summary>
58+
/// Optional: Enable Google Search grounding for web search capabilities (default: false).
59+
/// When enabled, Gemini can search the web to provide current and factual information.
60+
/// </summary>
61+
[DefaultValue(false)]
62+
public bool EnableGrounding { get; set; } = false;
63+
64+
/// <summary>
65+
/// Optional: Dynamic retrieval threshold for grounding (0.0 to 1.0, default: 0.7).
66+
/// The model will only perform a web search if its confidence in answering from its own knowledge
67+
/// falls below this threshold. Set to 1.0 to always search, or 0.0 to never search.
68+
/// </summary>
69+
[DefaultValue(0.7)]
70+
public double GroundingThreshold { get; set; } = 0.7;
71+
72+
/// <summary>
73+
/// Optional: Grounding mode ("DYNAMIC" for dynamic threshold, "ALWAYS" to always ground).
74+
/// Default: "DYNAMIC" - only searches when confidence is below threshold.
75+
/// </summary>
76+
[DefaultValue("DYNAMIC")]
77+
public string GroundingMode { get; set; } = "DYNAMIC";
5678
}

CommonUtilities/Helpers/GoogleAI/GeminiHelper.cs

Lines changed: 234 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Text;
16+
using System.Text.Json;
1517
using GenerativeAI;
1618
using GenerativeAI.Types;
1719

@@ -20,12 +22,15 @@ namespace CommonUtilities.Helpers.GoogleAI;
2022
/// <summary>
2123
/// Helper for Google Gemini AI models.
2224
/// Supports text generation, chat, streaming, and multimodal input (text + images/files).
25+
/// Includes grounding with Google Search for web-based information retrieval.
2326
/// Reference: https://github.com/gunpal5/google_generativeai
2427
/// </summary>
2528
public class GeminiHelper : IGeminiHelper
2629
{
30+
private const string GeminiApiUrl = "https://generativelanguage.googleapis.com/v1beta/models";
2731
private readonly GeminiConfig _config;
2832
private readonly GoogleAi _googleAi;
33+
private readonly HttpClient _httpClient;
2934
private readonly GenerativeModel _model;
3035

3136
/// <summary>
@@ -36,24 +41,50 @@ public GeminiHelper(GeminiConfig config)
3641
_config = config ?? throw new ArgumentNullException(nameof(config));
3742
_googleAi = new GoogleAi(_config.ApiKey);
3843
_model = _googleAi.CreateGenerativeModel(_config.Model);
44+
_httpClient = new HttpClient();
3945
}
4046

4147
/// <summary>
4248
/// Generates text content from a prompt using the configured Gemini model.
4349
/// </summary>
4450
public async Task<string> GenerateTextAsync(string prompt)
4551
{
46-
var response = await _model.GenerateContentAsync(prompt);
52+
GenerateContentResponse? response = await _model.GenerateContentAsync(prompt);
4753
return response?.Text() ?? string.Empty;
4854
}
4955

56+
/// <summary>
57+
/// Generates text with grounding enabled (Google Search).
58+
/// This allows the model to search the web for current and factual information.
59+
/// </summary>
60+
/// <param name="prompt">The prompt to send to the model</param>
61+
/// <returns>Tuple of (response text, grounding metadata if available)</returns>
62+
public async Task<(string responseText, GroundingMetadata? metadata)> GenerateTextWithGroundingAsync(string prompt)
63+
{
64+
try
65+
{
66+
// Only use grounding if enabled in config
67+
if (!_config.EnableGrounding)
68+
return (await GenerateTextAsync(prompt), null);
69+
70+
// Use REST API for grounding support
71+
return await CallGeminiWithGroundingAsync(prompt);
72+
}
73+
catch (Exception ex)
74+
{
75+
// If grounding fails, fall back to regular generation
76+
Console.WriteLine($"Grounding error: {ex.Message}. Falling back to regular generation.");
77+
return (await GenerateTextAsync(prompt), null);
78+
}
79+
}
80+
5081
/// <summary>
5182
/// Starts a chat session and sends a message.
5283
/// </summary>
5384
public async Task<string> SendChatMessageAsync(string message)
5485
{
55-
var chat = _model.StartChat();
56-
var response = await chat.GenerateContentAsync(message);
86+
ChatSession chat = _model.StartChat();
87+
GenerateContentResponse? response = await chat.GenerateContentAsync(message);
5788
return response?.Text() ?? string.Empty;
5889
}
5990

@@ -62,7 +93,8 @@ public async Task<string> SendChatMessageAsync(string message)
6293
/// </summary>
6394
public async IAsyncEnumerable<string> StreamTextAsync(string prompt)
6495
{
65-
await foreach (var chunk in _model.StreamContentAsync(prompt)) yield return chunk?.Text() ?? string.Empty;
96+
await foreach (GenerateContentResponse? chunk in _model.StreamContentAsync(prompt))
97+
yield return chunk?.Text() ?? string.Empty;
6698
}
6799

68100
/// <summary>
@@ -72,10 +104,10 @@ public async IAsyncEnumerable<string> StreamTextAsync(string prompt)
72104
/// </summary>
73105
public async Task<string> GenerateMultimodalContentAsync(string prompt, string filePath)
74106
{
75-
var request = new GenerateContentRequest();
107+
GenerateContentRequest request = new();
76108
request.AddText(prompt);
77109
request.AddInlineFile(filePath);
78-
var response = await _model.GenerateContentAsync(request);
110+
GenerateContentResponse? response = await _model.GenerateContentAsync(request);
79111
return response?.Text() ?? string.Empty;
80112
}
81113

@@ -87,4 +119,200 @@ public Task<string> GetModelInfoAsync(string modelId)
87119
throw new NotSupportedException(
88120
"Model info retrieval is not supported by the current Google_GenerativeAI SDK.");
89121
}
122+
123+
/// <summary>
124+
/// Calls the Gemini API with grounding tools via REST API.
125+
/// This method bypasses the SDK to access grounding features directly.
126+
/// </summary>
127+
private async Task<(string responseText, GroundingMetadata? metadata)> CallGeminiWithGroundingAsync(string prompt)
128+
{
129+
try
130+
{
131+
// Build the request body with grounding tools
132+
object requestBody = BuildGroundingRequest(prompt);
133+
134+
// Extract model name without "models/" prefix if present
135+
string modelId = _config.Model.Replace("models/", "");
136+
string url = $"{GeminiApiUrl}/{modelId}:generateContent?key={_config.ApiKey}";
137+
138+
// Make the API call
139+
StringContent content = new(
140+
JsonSerializer.Serialize(requestBody),
141+
Encoding.UTF8,
142+
"application/json"
143+
);
144+
145+
HttpResponseMessage response = await _httpClient.PostAsync(url, content);
146+
response.EnsureSuccessStatusCode();
147+
148+
string responseContent = await response.Content.ReadAsStringAsync();
149+
JsonDocument jsonDocument = JsonDocument.Parse(responseContent);
150+
JsonElement root = jsonDocument.RootElement;
151+
152+
// Extract the text response
153+
string responseText = string.Empty;
154+
if (root.TryGetProperty("candidates", out JsonElement candidates) && candidates.GetArrayLength() > 0)
155+
{
156+
JsonElement firstCandidate = candidates[0];
157+
if (firstCandidate.TryGetProperty("content", out JsonElement contentElement) &&
158+
contentElement.TryGetProperty("parts", out JsonElement parts) && parts.GetArrayLength() > 0)
159+
if (parts[0].TryGetProperty("text", out JsonElement textElement))
160+
responseText = textElement.GetString() ?? string.Empty;
161+
162+
// Extract grounding metadata if available
163+
GroundingMetadata? metadata = ExtractGroundingMetadata(firstCandidate);
164+
return (responseText, metadata);
165+
}
166+
167+
return (responseText, null);
168+
}
169+
catch (HttpRequestException ex)
170+
{
171+
Console.WriteLine($"HTTP error in grounding call: {ex.Message}");
172+
throw;
173+
}
174+
}
175+
176+
/// <summary>
177+
/// Builds the request body for Gemini API with grounding tools.
178+
/// </summary>
179+
private object BuildGroundingRequest(string prompt)
180+
{
181+
// Build tools array based on grounding mode
182+
List<object> tools = new();
183+
184+
if (_config.GroundingMode == "DYNAMIC")
185+
// Dynamic grounding with threshold
186+
tools.Add(new
187+
{
188+
google_search_retrieval = new
189+
{
190+
dynamic_retrieval_config = new
191+
{
192+
mode = "MODE_DYNAMIC",
193+
dynamic_threshold = _config.GroundingThreshold
194+
}
195+
}
196+
});
197+
else if (_config.GroundingMode == "ALWAYS")
198+
// Always perform web search
199+
tools.Add(new { google_search = new { } });
200+
201+
return new
202+
{
203+
contents = new[]
204+
{
205+
new
206+
{
207+
parts = new[]
208+
{
209+
new { text = prompt }
210+
}
211+
}
212+
},
213+
tools
214+
};
215+
}
216+
217+
/// <summary>
218+
/// Extracts grounding metadata from the Gemini API response.
219+
/// </summary>
220+
private GroundingMetadata? ExtractGroundingMetadata(JsonElement candidate)
221+
{
222+
if (!candidate.TryGetProperty("groundingMetadata", out JsonElement metadata))
223+
return null;
224+
225+
GroundingMetadata groundingData = new();
226+
227+
// Extract search queries
228+
if (metadata.TryGetProperty("searchQueries", out JsonElement queries))
229+
foreach (JsonElement query in queries.EnumerateArray())
230+
if (query.TryGetProperty("text", out JsonElement queryText))
231+
groundingData.SearchQueries.Add(new SearchQuery
232+
{
233+
Text = queryText.GetString() ?? string.Empty
234+
});
235+
236+
// Extract web results
237+
if (metadata.TryGetProperty("webResults", out JsonElement results))
238+
foreach (JsonElement result in results.EnumerateArray())
239+
{
240+
WebResult webResult = new();
241+
if (result.TryGetProperty("url", out JsonElement url))
242+
webResult.Url = url.GetString() ?? string.Empty;
243+
if (result.TryGetProperty("title", out JsonElement title))
244+
webResult.Title = title.GetString() ?? string.Empty;
245+
if (result.TryGetProperty("snippet", out JsonElement snippet))
246+
webResult.Snippet = snippet.GetString() ?? string.Empty;
247+
groundingData.WebResults.Add(webResult);
248+
}
249+
250+
// Extract citations
251+
if (metadata.TryGetProperty("citations", out JsonElement citations))
252+
foreach (JsonElement citation in citations.EnumerateArray())
253+
{
254+
Citation citationData = new();
255+
if (citation.TryGetProperty("startIndex", out JsonElement startIndex))
256+
citationData.StartIndex = startIndex.GetInt32();
257+
if (citation.TryGetProperty("endIndex", out JsonElement endIndex))
258+
citationData.EndIndex = endIndex.GetInt32();
259+
if (citation.TryGetProperty("uri", out JsonElement uri))
260+
citationData.Uri = uri.GetString() ?? string.Empty;
261+
groundingData.Citations.Add(citationData);
262+
}
263+
264+
return groundingData.SearchQueries.Count > 0 || groundingData.WebResults.Count > 0
265+
? groundingData
266+
: null;
267+
}
268+
}
269+
270+
/// <summary>
271+
/// Represents grounding metadata returned from Gemini when using grounding tools.
272+
/// Contains information about web search queries, results, and citations.
273+
/// </summary>
274+
public class GroundingMetadata
275+
{
276+
/// <summary>
277+
/// Search queries generated and executed by the model.
278+
/// </summary>
279+
public List<SearchQuery> SearchQueries { get; set; } = new();
280+
281+
/// <summary>
282+
/// Web search results retrieved for grounding.
283+
/// </summary>
284+
public List<WebResult> WebResults { get; set; } = new();
285+
286+
/// <summary>
287+
/// Citation information embedded in the response.
288+
/// </summary>
289+
public List<Citation> Citations { get; set; } = new();
290+
}
291+
292+
/// <summary>
293+
/// Represents a search query generated by the model for grounding.
294+
/// </summary>
295+
public class SearchQuery
296+
{
297+
public string Text { get; set; } = string.Empty;
298+
}
299+
300+
/// <summary>
301+
/// Represents a web search result used for grounding.
302+
/// </summary>
303+
public class WebResult
304+
{
305+
public string Url { get; set; } = string.Empty;
306+
public string Title { get; set; } = string.Empty;
307+
public string Snippet { get; set; } = string.Empty;
308+
}
309+
310+
/// <summary>
311+
/// Represents a citation in the grounded response.
312+
/// </summary>
313+
public class Citation
314+
{
315+
public int StartIndex { get; set; }
316+
public int EndIndex { get; set; }
317+
public string Uri { get; set; } = string.Empty;
90318
}

CommonUtilities/Helpers/GoogleAI/IGeminiHelper.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace CommonUtilities.Helpers.GoogleAI;
1717
/// <summary>
1818
/// Interface for Google Gemini helper.
1919
/// Gemini models support text generation, chat, streaming, and multimodal input (text + images/files).
20+
/// Includes support for grounding with Google Search for web-based information.
2021
/// Reference: https://github.com/gunpal5/google_generativeai#usage
2122
/// </summary>
2223
public interface IGeminiHelper
@@ -26,6 +27,14 @@ public interface IGeminiHelper
2627
/// </summary>
2728
Task<string> GenerateTextAsync(string prompt);
2829

30+
/// <summary>
31+
/// Generates text with grounding enabled (Google Search).
32+
/// This allows the model to search the web for current and factual information.
33+
/// </summary>
34+
/// <param name="prompt">The prompt to send to the model</param>
35+
/// <returns>Tuple of (response text, grounding metadata if available)</returns>
36+
Task<(string responseText, GroundingMetadata? metadata)> GenerateTextWithGroundingAsync(string prompt);
37+
2938
/// <summary>
3039
/// Starts a chat session and sends a message.
3140
/// </summary>

0 commit comments

Comments
 (0)