Skip to content

Commit d76e5fa

Browse files
authored
[FirebaseAI] Add support for Grounding with Google Search (#1285)
* [FirebaseAI] Add support for Grounding with Google Search * Review fixes
1 parent 5e1c3b4 commit d76e5fa

File tree

4 files changed

+456
-12
lines changed

4 files changed

+456
-12
lines changed

firebaseai/src/Candidate.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,20 @@ public IEnumerable<SafetyRating> SafetyRatings {
100100
/// </summary>
101101
public CitationMetadata? CitationMetadata { get; }
102102

103+
/// <summary>
104+
/// Grounding metadata for the response, if any.
105+
/// </summary>
106+
public GroundingMetadata? GroundingMetadata { get; }
107+
103108
// Hidden constructor, users don't need to make this.
104109
private Candidate(ModelContent content, List<SafetyRating> safetyRatings,
105-
FinishReason? finishReason, CitationMetadata? citationMetadata) {
110+
FinishReason? finishReason, CitationMetadata? citationMetadata,
111+
GroundingMetadata? groundingMetadata) {
106112
Content = content;
107113
_safetyRatings = new ReadOnlyCollection<SafetyRating>(safetyRatings ?? new List<SafetyRating>());
108114
FinishReason = finishReason;
109115
CitationMetadata = citationMetadata;
116+
GroundingMetadata = groundingMetadata;
110117
}
111118

112119
private static FinishReason ParseFinishReason(string str) {
@@ -135,7 +142,9 @@ internal static Candidate FromJson(Dictionary<string, object> jsonDict,
135142
jsonDict.ParseObjectList("safetyRatings", SafetyRating.FromJson),
136143
jsonDict.ParseNullableEnum("finishReason", ParseFinishReason),
137144
jsonDict.ParseNullableObject("citationMetadata",
138-
(d) => Firebase.AI.CitationMetadata.FromJson(d, backend)));
145+
(d) => Firebase.AI.CitationMetadata.FromJson(d, backend)),
146+
jsonDict.ParseNullableObject("groundingMetadata",
147+
Firebase.AI.GroundingMetadata.FromJson));
139148
}
140149
}
141150

firebaseai/src/FunctionCalling.cs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ internal Dictionary<string, object> ToJson() {
7070
}
7171
}
7272

73+
/// <summary>
74+
/// A tool that allows the generative model to connect to Google Search to access and incorporate
75+
/// up-to-date information from the web into its responses.
76+
///
77+
/// > Important: When using this feature, you are required to comply with the
78+
/// "Grounding with Google Search" usage requirements for your chosen API provider:
79+
/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search)
80+
/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms)
81+
/// section within the Service Specific Terms).
82+
/// </summary>
83+
public readonly struct GoogleSearch {}
84+
7385
/// <summary>
7486
/// A helper tool that the model may use when generating responses.
7587
///
@@ -79,33 +91,51 @@ internal Dictionary<string, object> ToJson() {
7991
public readonly struct Tool {
8092
// No public properties, on purpose since it is meant for user input only
8193

82-
private List<FunctionDeclaration> Functions { get; }
94+
private List<FunctionDeclaration> FunctionDeclarations { get; }
95+
private GoogleSearch? GoogleSearch { get; }
8396

8497
/// <summary>
8598
/// Creates a tool that allows the model to perform function calling.
8699
/// </summary>
87100
/// <param name="functionDeclarations">A list of `FunctionDeclarations` available to the model
88101
/// that can be used for function calling.</param>
89102
public Tool(params FunctionDeclaration[] functionDeclarations) {
90-
Functions = new List<FunctionDeclaration>(functionDeclarations);
103+
FunctionDeclarations = new List<FunctionDeclaration>(functionDeclarations);
104+
GoogleSearch = null;
91105
}
92106
/// <summary>
93107
/// Creates a tool that allows the model to perform function calling.
94108
/// </summary>
95109
/// <param name="functionDeclarations">A list of `FunctionDeclarations` available to the model
96110
/// that can be used for function calling.</param>
97111
public Tool(IEnumerable<FunctionDeclaration> functionDeclarations) {
98-
Functions = new List<FunctionDeclaration>(functionDeclarations);
112+
FunctionDeclarations = new List<FunctionDeclaration>(functionDeclarations);
113+
GoogleSearch = null;
114+
}
115+
116+
/// <summary>
117+
/// Creates a tool that allows the model to use Grounding with Google Search.
118+
/// </summary>
119+
/// <param name="googleSearch">An empty `GoogleSearch` object. The presence of this object
120+
/// in the list of tools enables the model to use Google Search.</param>
121+
public Tool(GoogleSearch googleSearch) {
122+
FunctionDeclarations = null;
123+
GoogleSearch = googleSearch;
99124
}
100125

101126
/// <summary>
102127
/// Intended for internal use only.
103128
/// This method is used for serializing the object to JSON for the API request.
104129
/// </summary>
105130
internal Dictionary<string, object> ToJson() {
106-
return new() {
107-
{ "functionDeclarations", Functions.Select(f => f.ToJson()).ToList() }
108-
};
131+
var json = new Dictionary<string, object>();
132+
if (FunctionDeclarations != null && FunctionDeclarations.Any()) {
133+
json["functionDeclarations"] = FunctionDeclarations.Select(f => f.ToJson()).ToList();
134+
}
135+
if (GoogleSearch.HasValue) {
136+
json["googleSearch"] = new Dictionary<string, object>();
137+
}
138+
return json;
109139
}
110140
}
111141

firebaseai/src/GenerateContentResponse.cs

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
using System;
1718
using System.Collections.Generic;
1819
using System.Collections.ObjectModel;
1920
using System.Linq;
@@ -180,6 +181,256 @@ internal static PromptFeedback FromJson(Dictionary<string, object> jsonDict) {
180181
}
181182
}
182183

184+
/// <summary>
185+
/// Metadata returned to the client when grounding is enabled.
186+
///
187+
/// > Important: If using Grounding with Google Search, you are required to comply with the
188+
/// "Grounding with Google Search" usage requirements for your chosen API provider:
189+
/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search)
190+
/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms)
191+
/// section within the Service Specific Terms).
192+
/// </summary>
193+
public readonly struct GroundingMetadata {
194+
private readonly ReadOnlyCollection<string> _webSearchQueries;
195+
private readonly ReadOnlyCollection<GroundingChunk> _groundingChunks;
196+
private readonly ReadOnlyCollection<GroundingSupport> _groundingSupports;
197+
198+
/// <summary>
199+
/// A list of web search queries that the model performed to gather the grounding information.
200+
/// These can be used to allow users to explore the search results themselves.
201+
/// </summary>
202+
public IEnumerable<string> WebSearchQueries {
203+
get {
204+
return _webSearchQueries ?? new ReadOnlyCollection<string>(new List<string>());
205+
}
206+
}
207+
208+
/// <summary>
209+
/// A list of `GroundingChunk` structs. Each chunk represents a piece of retrieved content
210+
/// (e.g., from a web page) that the model used to ground its response.
211+
/// </summary>
212+
public IEnumerable<GroundingChunk> GroundingChunks {
213+
get {
214+
return _groundingChunks ?? new ReadOnlyCollection<GroundingChunk>(new List<GroundingChunk>());
215+
}
216+
}
217+
218+
/// <summary>
219+
/// A list of `GroundingSupport` structs. Each object details how specific segments of the
220+
/// model's response are supported by the `groundingChunks`.
221+
/// </summary>
222+
public IEnumerable<GroundingSupport> GroundingSupports {
223+
get {
224+
return _groundingSupports ?? new ReadOnlyCollection<GroundingSupport>(new List<GroundingSupport>());
225+
}
226+
}
227+
228+
/// <summary>
229+
/// Google Search entry point for web searches.
230+
/// This contains an HTML/CSS snippet that **must** be embedded in an app to display a Google
231+
/// Search entry point for follow-up web searches related to the model's "Grounded Response".
232+
/// </summary>
233+
public SearchEntryPoint? SearchEntryPoint { get; }
234+
235+
private GroundingMetadata(List<string> webSearchQueries, List<GroundingChunk> groundingChunks,
236+
List<GroundingSupport> groundingSupports, SearchEntryPoint? searchEntryPoint) {
237+
_webSearchQueries = new ReadOnlyCollection<string>(webSearchQueries ?? new List<string>());
238+
_groundingChunks = new ReadOnlyCollection<GroundingChunk>(groundingChunks ?? new List<GroundingChunk>());
239+
_groundingSupports = new ReadOnlyCollection<GroundingSupport>(groundingSupports ?? new List<GroundingSupport>());
240+
SearchEntryPoint = searchEntryPoint;
241+
}
242+
243+
internal static GroundingMetadata FromJson(Dictionary<string, object> jsonDict) {
244+
List<GroundingSupport> supports = null;
245+
if (jsonDict.TryParseValue("groundingSupports", out List<object> supportListRaw))
246+
{
247+
supports = supportListRaw
248+
.OfType<Dictionary<string, object>>()
249+
.Where(d => d.ContainsKey("segment")) // Filter out if segment is missing
250+
.Select(GroundingSupport.FromJson)
251+
.ToList();
252+
}
253+
254+
return new GroundingMetadata(
255+
jsonDict.ParseStringList("webSearchQueries"),
256+
jsonDict.ParseObjectList("groundingChunks", GroundingChunk.FromJson),
257+
supports,
258+
jsonDict.ParseNullableObject("searchEntryPoint", Firebase.AI.SearchEntryPoint.FromJson)
259+
);
260+
}
261+
}
262+
263+
/// <summary>
264+
/// A struct representing the Google Search entry point.
265+
/// </summary>
266+
public readonly struct SearchEntryPoint {
267+
/// <summary>
268+
/// An HTML/CSS snippet that can be embedded in your app.
269+
///
270+
/// To ensure proper rendering, it's recommended to display this content within a web view component.
271+
/// </summary>
272+
public string RenderedContent { get; }
273+
274+
private SearchEntryPoint(string renderedContent) {
275+
RenderedContent = renderedContent;
276+
}
277+
278+
internal static SearchEntryPoint FromJson(Dictionary<string, object> jsonDict) {
279+
return new SearchEntryPoint(
280+
jsonDict.ParseValue<string>("renderedContent", JsonParseOptions.ThrowEverything)
281+
);
282+
}
283+
}
284+
285+
/// <summary>
286+
/// Represents a chunk of retrieved data that supports a claim in the model's response. This is
287+
/// part of the grounding information provided when grounding is enabled.
288+
/// </summary>
289+
public readonly struct GroundingChunk {
290+
/// <summary>
291+
/// Contains details if the grounding chunk is from a web source.
292+
/// </summary>
293+
public WebGroundingChunk? Web { get; }
294+
295+
private GroundingChunk(WebGroundingChunk? web) {
296+
Web = web;
297+
}
298+
299+
internal static GroundingChunk FromJson(Dictionary<string, object> jsonDict) {
300+
return new GroundingChunk(
301+
jsonDict.ParseNullableObject("web", WebGroundingChunk.FromJson)
302+
);
303+
}
304+
}
305+
306+
/// <summary>
307+
/// A grounding chunk sourced from the web.
308+
/// </summary>
309+
public readonly struct WebGroundingChunk {
310+
/// <summary>
311+
/// The URI of the retrieved web page.
312+
/// </summary>
313+
public System.Uri Uri { get; }
314+
/// <summary>
315+
/// The title of the retrieved web page.
316+
/// </summary>
317+
public string Title { get; }
318+
/// <summary>
319+
/// The domain of the original URI from which the content was retrieved.
320+
///
321+
/// This field is only populated when using the Vertex AI Gemini API.
322+
/// </summary>
323+
public string Domain { get; }
324+
325+
private WebGroundingChunk(System.Uri uri, string title, string domain) {
326+
Uri = uri;
327+
Title = title;
328+
Domain = domain;
329+
}
330+
331+
internal static WebGroundingChunk FromJson(Dictionary<string, object> jsonDict) {
332+
Uri uri = null;
333+
if (jsonDict.TryParseValue("uri", out string uriString)) {
334+
uri = new Uri(uriString);
335+
}
336+
337+
return new WebGroundingChunk(
338+
uri,
339+
jsonDict.ParseValue<string>("title"),
340+
jsonDict.ParseValue<string>("domain")
341+
);
342+
}
343+
}
344+
345+
/// <summary>
346+
/// Provides information about how a specific segment of the model's response is supported by the
347+
/// retrieved grounding chunks.
348+
/// </summary>
349+
public readonly struct GroundingSupport {
350+
private readonly ReadOnlyCollection<int> _groundingChunkIndices;
351+
352+
/// <summary>
353+
/// Specifies the segment of the model's response content that this grounding support pertains
354+
/// to.
355+
/// </summary>
356+
public Segment Segment { get; }
357+
358+
/// <summary>
359+
/// A list of indices that refer to specific `GroundingChunk` structs within the
360+
/// `GroundingMetadata.GroundingChunks` array. These referenced chunks are the sources that
361+
/// support the claim made in the associated `segment` of the response. For example, an array
362+
/// `[1, 3, 4]`
363+
/// means that `groundingChunks[1]`, `groundingChunks[3]`, `groundingChunks[4]` are the
364+
/// retrieved content supporting this part of the response.
365+
/// </summary>
366+
public IEnumerable<int> GroundingChunkIndices {
367+
get {
368+
return _groundingChunkIndices ?? new ReadOnlyCollection<int>(new List<int>());
369+
}
370+
}
371+
372+
private GroundingSupport(Segment segment, List<int> groundingChunkIndices) {
373+
Segment = segment;
374+
_groundingChunkIndices = new ReadOnlyCollection<int>(groundingChunkIndices ?? new List<int>());
375+
}
376+
377+
internal static GroundingSupport FromJson(Dictionary<string, object> jsonDict) {
378+
List<int> indices = new List<int>();
379+
if (jsonDict.TryParseValue("groundingChunkIndices", out List<object> indicesRaw)) {
380+
indices = indicesRaw.OfType<long>().Select(l => (int)l).ToList();
381+
}
382+
383+
return new GroundingSupport(
384+
jsonDict.ParseObject("segment", Segment.FromJson, JsonParseOptions.ThrowEverything),
385+
indices
386+
);
387+
}
388+
}
389+
390+
/// <summary>
391+
/// Represents a specific segment within a `ModelContent` struct, often used to pinpoint the
392+
/// exact location of text or data that grounding information refers to.
393+
/// </summary>
394+
public readonly struct Segment {
395+
/// <summary>
396+
/// The zero-based index of the `Part` object within the `parts` array of its parent
397+
/// `ModelContent` object. This identifies which part of the content the segment belongs to.
398+
/// </summary>
399+
public int PartIndex { get; }
400+
/// <summary>
401+
/// The zero-based start index of the segment within the specified `Part`, measured in UTF-8
402+
/// bytes. This offset is inclusive, starting from 0 at the beginning of the part's content.
403+
/// </summary>
404+
public int StartIndex { get; }
405+
/// <summary>
406+
/// The zero-based end index of the segment within the specified `Part`, measured in UTF-8
407+
/// bytes. This offset is exclusive, meaning the character at this index is not included in the
408+
/// segment.
409+
/// </summary>
410+
public int EndIndex { get; }
411+
/// <summary>
412+
/// The text corresponding to the segment from the response.
413+
/// </summary>
414+
public string Text { get; }
415+
416+
private Segment(int partIndex, int startIndex, int endIndex, string text) {
417+
PartIndex = partIndex;
418+
StartIndex = startIndex;
419+
EndIndex = endIndex;
420+
Text = text;
421+
}
422+
423+
internal static Segment FromJson(Dictionary<string, object> jsonDict) {
424+
return new Segment(
425+
jsonDict.ParseValue<int>("partIndex"),
426+
jsonDict.ParseValue<int>("startIndex"),
427+
jsonDict.ParseValue<int>("endIndex"),
428+
jsonDict.ParseValue<string>("text")
429+
);
430+
}
431+
}
432+
433+
183434
/// <summary>
184435
/// Token usage metadata for processing the generate content request.
185436
/// </summary>

0 commit comments

Comments
 (0)