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 ;
1517using GenerativeAI ;
1618using 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>
2528public 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}
0 commit comments