From 5bfc5db48ef4d854d7b493333e1336303681befc Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 8 Jul 2025 16:44:51 -0400 Subject: [PATCH 1/2] [FirebaseAI] Add support for Grounding with Google Search --- firebaseai/src/Candidate.cs | 13 +- firebaseai/src/FunctionCalling.cs | 42 ++- firebaseai/src/GenerateContentResponse.cs | 246 ++++++++++++++++++ .../Sample/FirebaseAI/UIHandlerAutomated.cs | 156 ++++++++++- 4 files changed, 446 insertions(+), 11 deletions(-) diff --git a/firebaseai/src/Candidate.cs b/firebaseai/src/Candidate.cs index b607c864..bc370942 100644 --- a/firebaseai/src/Candidate.cs +++ b/firebaseai/src/Candidate.cs @@ -100,13 +100,20 @@ public IEnumerable SafetyRatings { /// public CitationMetadata? CitationMetadata { get; } + /// + /// Grounding metadata for the response, if any. + /// + public GroundingMetadata? GroundingMetadata { get; } + // Hidden constructor, users don't need to make this. private Candidate(ModelContent content, List safetyRatings, - FinishReason? finishReason, CitationMetadata? citationMetadata) { + FinishReason? finishReason, CitationMetadata? citationMetadata, + GroundingMetadata? groundingMetadata) { Content = content; _safetyRatings = new ReadOnlyCollection(safetyRatings ?? new List()); FinishReason = finishReason; CitationMetadata = citationMetadata; + GroundingMetadata = groundingMetadata; } private static FinishReason ParseFinishReason(string str) { @@ -135,7 +142,9 @@ internal static Candidate FromJson(Dictionary jsonDict, jsonDict.ParseObjectList("safetyRatings", SafetyRating.FromJson), jsonDict.ParseNullableEnum("finishReason", ParseFinishReason), jsonDict.ParseNullableObject("citationMetadata", - (d) => Firebase.AI.CitationMetadata.FromJson(d, backend))); + (d) => Firebase.AI.CitationMetadata.FromJson(d, backend)), + jsonDict.ParseNullableObject("groundingMetadata", + Firebase.AI.GroundingMetadata.FromJson)); } } diff --git a/firebaseai/src/FunctionCalling.cs b/firebaseai/src/FunctionCalling.cs index 2fc54d95..247597ce 100644 --- a/firebaseai/src/FunctionCalling.cs +++ b/firebaseai/src/FunctionCalling.cs @@ -70,6 +70,18 @@ internal Dictionary ToJson() { } } +/// +/// A tool that allows the generative model to connect to Google Search to access and incorporate +/// up-to-date information from the web into its responses. +/// +/// > Important: When using this feature, you are required to comply with the +/// "Grounding with Google Search" usage requirements for your chosen API provider: +/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) +/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) +/// section within the Service Specific Terms). +/// +public readonly struct GoogleSearch {} + /// /// A helper tool that the model may use when generating responses. /// @@ -79,7 +91,8 @@ internal Dictionary ToJson() { public readonly struct Tool { // No public properties, on purpose since it is meant for user input only - private List Functions { get; } + private List FunctionDeclarations { get; } + private GoogleSearch? GoogleSearch { get; } /// /// Creates a tool that allows the model to perform function calling. @@ -87,7 +100,8 @@ public readonly struct Tool { /// A list of `FunctionDeclarations` available to the model /// that can be used for function calling. public Tool(params FunctionDeclaration[] functionDeclarations) { - Functions = new List(functionDeclarations); + FunctionDeclarations = new List(functionDeclarations); + GoogleSearch = null; } /// /// Creates a tool that allows the model to perform function calling. @@ -95,7 +109,18 @@ public Tool(params FunctionDeclaration[] functionDeclarations) { /// A list of `FunctionDeclarations` available to the model /// that can be used for function calling. public Tool(IEnumerable functionDeclarations) { - Functions = new List(functionDeclarations); + FunctionDeclarations = new List(functionDeclarations); + GoogleSearch = null; + } + + /// + /// Creates a tool that allows the model to use Grounding with Google Search. + /// + /// An empty `GoogleSearch` object. The presence of this object + /// in the list of tools enables the model to use Google Search. + public Tool(GoogleSearch googleSearch) { + FunctionDeclarations = null; + GoogleSearch = googleSearch; } /// @@ -103,9 +128,14 @@ public Tool(IEnumerable functionDeclarations) { /// This method is used for serializing the object to JSON for the API request. /// internal Dictionary ToJson() { - return new() { - { "functionDeclarations", Functions.Select(f => f.ToJson()).ToList() } - }; + var json = new Dictionary(); + if (FunctionDeclarations != null && FunctionDeclarations.Any()) { + json["functionDeclarations"] = FunctionDeclarations.Select(f => f.ToJson()).ToList(); + } + if (GoogleSearch.HasValue) { + json["googleSearch"] = new Dictionary(); + } + return json; } } diff --git a/firebaseai/src/GenerateContentResponse.cs b/firebaseai/src/GenerateContentResponse.cs index 17ac497c..13133d25 100644 --- a/firebaseai/src/GenerateContentResponse.cs +++ b/firebaseai/src/GenerateContentResponse.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -180,6 +181,251 @@ internal static PromptFeedback FromJson(Dictionary jsonDict) { } } +/// +/// Metadata returned to the client when grounding is enabled. +/// +/// > Important: If using Grounding with Google Search, you are required to comply with the +/// "Grounding with Google Search" usage requirements for your chosen API provider: +/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) +/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) +/// section within the Service Specific Terms). +/// +public readonly struct GroundingMetadata { + private readonly ReadOnlyCollection _webSearchQueries; + private readonly ReadOnlyCollection _groundingChunks; + private readonly ReadOnlyCollection _groundingSupports; + + /// + /// A list of web search queries that the model performed to gather the grounding information. + /// These can be used to allow users to explore the search results themselves. + /// + public IEnumerable WebSearchQueries { + get { + return _webSearchQueries ?? new ReadOnlyCollection(new List()); + } + } + + /// + /// A list of `GroundingChunk` structs. Each chunk represents a piece of retrieved content + /// (e.g., from a web page) that the model used to ground its response. + /// + public IEnumerable GroundingChunks { + get { + return _groundingChunks ?? new ReadOnlyCollection(new List()); + } + } + + /// + /// A list of `GroundingSupport` structs. Each object details how specific segments of the + /// model's response are supported by the `groundingChunks`. + /// + public IEnumerable GroundingSupports { + get { + return _groundingSupports ?? new ReadOnlyCollection(new List()); + } + } + + /// + /// Google Search entry point for web searches. + /// This contains an HTML/CSS snippet that **must** be embedded in an app to display a Google + /// Search entry point for follow-up web searches related to the model's "Grounded Response". + /// + public SearchEntryPoint? SearchEntryPoint { get; } + + private GroundingMetadata(List webSearchQueries, List groundingChunks, + List groundingSupports, SearchEntryPoint? searchEntryPoint) { + _webSearchQueries = new ReadOnlyCollection(webSearchQueries ?? new List()); + _groundingChunks = new ReadOnlyCollection(groundingChunks ?? new List()); + _groundingSupports = new ReadOnlyCollection(groundingSupports ?? new List()); + SearchEntryPoint = searchEntryPoint; + } + + internal static GroundingMetadata FromJson(Dictionary jsonDict) { + List supports = null; + if (jsonDict.TryParseValue("groundingSupports", out List supportListRaw)) + { + supports = supportListRaw + .OfType>() + .Where(d => d.ContainsKey("segment")) // Filter out if segment is missing + .Select(GroundingSupport.FromJson) + .ToList(); + } + + return new GroundingMetadata( + jsonDict.ParseStringList("webSearchQueries"), + jsonDict.ParseObjectList("groundingChunks", GroundingChunk.FromJson), + supports, + jsonDict.ParseNullableObject("searchEntryPoint", Firebase.AI.SearchEntryPoint.FromJson) + ); + } +} + +/// +/// A struct representing the Google Search entry point. +/// +public readonly struct SearchEntryPoint { + /// + /// An HTML/CSS snippet that can be embedded in your app. + /// + /// To ensure proper rendering, it's recommended to display this content within a web view component. + /// + public string RenderedContent { get; } + + private SearchEntryPoint(string renderedContent) { + RenderedContent = renderedContent; + } + + internal static SearchEntryPoint FromJson(Dictionary jsonDict) { + return new SearchEntryPoint( + jsonDict.ParseValue("renderedContent", JsonParseOptions.ThrowEverything) + ); + } +} + +/// +/// Represents a chunk of retrieved data that supports a claim in the model's response. This is +/// part of the grounding information provided when grounding is enabled. +/// +public readonly struct GroundingChunk { + /// + /// Contains details if the grounding chunk is from a web source. + /// + public WebGroundingChunk? Web { get; } + + private GroundingChunk(WebGroundingChunk? web) { + Web = web; + } + + internal static GroundingChunk FromJson(Dictionary jsonDict) { + return new GroundingChunk( + jsonDict.ParseNullableObject("web", WebGroundingChunk.FromJson) + ); + } +} + +/// +/// A grounding chunk sourced from the web. +/// +public readonly struct WebGroundingChunk { + /// + /// The URI of the retrieved web page. + /// + public string Uri { get; } + /// + /// The title of the retrieved web page. + /// + public string Title { get; } + /// + /// The domain of the original URI from which the content was retrieved. + /// + /// This field is only populated when using the Vertex AI Gemini API. + /// + public string Domain { get; } + + private WebGroundingChunk(string uri, string title, string domain) { + Uri = uri; + Title = title; + Domain = domain; + } + + internal static WebGroundingChunk FromJson(Dictionary jsonDict) { + return new WebGroundingChunk( + jsonDict.ParseValue("uri"), + jsonDict.ParseValue("title"), + jsonDict.ParseValue("domain") + ); + } +} + +/// +/// Provides information about how a specific segment of the model's response is supported by the +/// retrieved grounding chunks. +/// +public readonly struct GroundingSupport { + private readonly ReadOnlyCollection _groundingChunkIndices; + + /// + /// Specifies the segment of the model's response content that this grounding support pertains + /// to. + /// + public Segment Segment { get; } + + /// + /// A list of indices that refer to specific `GroundingChunk` structs within the + /// `GroundingMetadata.GroundingChunks` array. These referenced chunks are the sources that + /// support the claim made in the associated `segment` of the response. For example, an array + /// `[1, 3, 4]` + /// means that `groundingChunks[1]`, `groundingChunks[3]`, `groundingChunks[4]` are the + /// retrieved content supporting this part of the response. + /// + public IEnumerable GroundingChunkIndices { + get { + return _groundingChunkIndices ?? new ReadOnlyCollection(new List()); + } + } + + private GroundingSupport(Segment segment, List groundingChunkIndices) { + Segment = segment; + _groundingChunkIndices = new ReadOnlyCollection(groundingChunkIndices ?? new List()); + } + + internal static GroundingSupport FromJson(Dictionary jsonDict) { + List indices = new List(); + if (jsonDict.TryParseValue("groundingChunkIndices", out List indicesRaw)) { + indices = indicesRaw.OfType().Select(l => (int)l).ToList(); + } + + return new GroundingSupport( + jsonDict.ParseObject("segment", Segment.FromJson, JsonParseOptions.ThrowEverything), + indices + ); + } +} + +/// +/// Represents a specific segment within a `ModelContent` struct, often used to pinpoint the +/// exact location of text or data that grounding information refers to. +/// +public readonly struct Segment { + /// + /// The zero-based index of the `Part` object within the `parts` array of its parent + /// `ModelContent` object. This identifies which part of the content the segment belongs to. + /// + public int PartIndex { get; } + /// + /// The zero-based start index of the segment within the specified `Part`, measured in UTF-8 + /// bytes. This offset is inclusive, starting from 0 at the beginning of the part's content. + /// + public int StartIndex { get; } + /// + /// The zero-based end index of the segment within the specified `Part`, measured in UTF-8 + /// bytes. This offset is exclusive, meaning the character at this index is not included in the + /// segment. + /// + public int EndIndex { get; } + /// + /// The text corresponding to the segment from the response. + /// + public string Text { get; } + + private Segment(int partIndex, int startIndex, int endIndex, string text) { + PartIndex = partIndex; + StartIndex = startIndex; + EndIndex = endIndex; + Text = text; + } + + internal static Segment FromJson(Dictionary jsonDict) { + return new Segment( + jsonDict.ParseValue("partIndex"), + jsonDict.ParseValue("startIndex"), + jsonDict.ParseValue("endIndex"), + jsonDict.ParseValue("text") + ); + } +} + + /// /// Token usage metadata for processing the generate content request. /// diff --git a/firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs b/firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs index 9a1a7006..32777a02 100644 --- a/firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs +++ b/firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs @@ -63,6 +63,7 @@ protected override void Start() { TestFunctionCallingNone, TestEnumSchemaResponse, TestAnyOfSchemaResponse, + TestSearchGrounding, TestChatBasicTextNoHistory, TestChatBasicTextPriorHistory, TestChatFunctionCalling, @@ -82,6 +83,11 @@ protected override void Start() { InternalTestFinishReasonSafetyNoContent, InternalTestUnknownEnumSafetyRatings, InternalTestFunctionCallWithArguments, + InternalTestVertexAIGrounding, + InternalTestGoogleAIGrounding, + InternalTestGoogleAIGroundingEmptyChunks, + InternalTestGroundingMetadata_Empty, + InternalTestSegment_Empty, InternalTestCountTokenResponse, InternalTestBasicResponseLongUsageMetadata, InternalTestGoogleAIBasicReplyShort, @@ -443,7 +449,7 @@ async Task TestEnumSchemaResponse(Backend backend) { generationConfig: new GenerationConfig( responseMimeType: "text/x.enum", responseSchema: Schema.Enum(new string[] { enumValue }))); - + var response = await model.GenerateContentAsync( "Hello, I am testing setting the response schema to an enum."); @@ -467,6 +473,43 @@ async Task TestAnyOfSchemaResponse(Backend backend) { Assert("Response was empty.", !string.IsNullOrWhiteSpace(response.Text)); } + // Test grounding with Google Search. + async Task TestSearchGrounding(Backend backend) { + // Use a model that supports grounding. + var model = GetFirebaseAI(backend).GetGenerativeModel(TestModelName, + tools: new Tool[] { new Tool(new GoogleSearch()) } + ); + + // A prompt that requires recent information. + GenerateContentResponse response = await model.GenerateContentAsync("What's the current weather in Toronto?"); + + Assert("Response missing candidates.", response.Candidates.Any()); + + string result = response.Text; + Assert("Response text was missing", !string.IsNullOrWhiteSpace(result)); + + var candidate = response.Candidates.First(); + Assert("GroundingMetadata should not be null when GoogleSearch tool is used.", + candidate.GroundingMetadata.HasValue); + + var groundingMetadata = candidate.GroundingMetadata.Value; + + Assert("WebSearchQueries should not be empty.", + groundingMetadata.WebSearchQueries.Any()); + + Assert("GroundingChunks should not be empty.", + groundingMetadata.GroundingChunks.Any()); + + Assert("GroundingSupports should not be empty.", + groundingMetadata.GroundingSupports.Any()); + + Assert("SearchEntryPoint should not be null.", + groundingMetadata.SearchEntryPoint.HasValue); + + Assert("SearchEntryPoint.RenderedContent should not be empty.", + !string.IsNullOrWhiteSpace(groundingMetadata.SearchEntryPoint?.RenderedContent)); + } + // Test if when using Chat the model will get the previous messages. async Task TestChatBasicTextNoHistory(Backend backend) { var model = CreateGenerativeModel(backend); @@ -732,7 +775,7 @@ async Task TestReadSecureFile() { // The url prefix to use when fetching test data to use from the separate GitHub repo. readonly string testDataUrl = - "https://raw.githubusercontent.com/FirebaseExtended/vertexai-sdk-test-data/47becf9101d11ea3c568bf60b12f1c8ed9fb684e/mock-responses/"; + "https://raw.githubusercontent.com/FirebaseExtended/vertexai-sdk-test-data/548c2d5ae4555ca6f57d8621903e2b591bec7b05/mock-responses/"; readonly HttpClient httpClient = new(); private Task LoadStreamingAsset(string fullPath) { @@ -1026,6 +1069,113 @@ async Task InternalTestFunctionCallWithArguments() { AssertEq("FunctionCall args[x] wrong value", fcPart.Args["x"], 4L); } + // Test that parsing a Vertex AI response with GroundingMetadata works. + // https://github.com/FirebaseExtended/vertexai-sdk-test-data/blob/main/mock-responses/vertexai/unary-success-google-search-grounding.json + async Task InternalTestVertexAIGrounding() { + Dictionary json = await GetVertexJsonTestData("unary-success-google-search-grounding.json"); + + GenerateContentResponse response = GenerateContentResponse.FromJson(json, FirebaseAI.Backend.InternalProvider.VertexAI); + + Assert("Response missing candidates.", response.Candidates.Any()); + var candidate = response.Candidates.First(); + Assert("Candidate should have GroundingMetadata", candidate.GroundingMetadata.HasValue); + + var grounding = candidate.GroundingMetadata.Value; + + Assert("WebSearchQueries should not be null", grounding.WebSearchQueries != null); + Assert("SearchEntryPoint should not be null", grounding.SearchEntryPoint.HasValue); + Assert("GroundingChunks should not be null", grounding.GroundingChunks != null); + var chunk = grounding.GroundingChunks.First(); + Assert("GroundingChunk.Web should not be null", chunk.Web.HasValue); + Assert("GroundingSupports should not be null", grounding.GroundingSupports != null); + var support = grounding.GroundingSupports.First(); + Assert("GroundingChunkIndices should not be null", support.GroundingChunkIndices != null); + } + + // Test that parsing a Google AI response with GroundingMetadata works. + // https://github.com/FirebaseExtended/vertexai-sdk-test-data/blob/main/mock-responses/googleai/unary-success-google-search-grounding.json + async Task InternalTestGoogleAIGrounding() { + Dictionary json = await GetGoogleAIJsonTestData("unary-success-google-search-grounding.json"); + GenerateContentResponse response = GenerateContentResponse.FromJson(json, FirebaseAI.Backend.InternalProvider.GoogleAI); + + Assert("Response missing candidates.", response.Candidates.Any()); + var candidate = response.Candidates.First(); + Assert("Candidate should have GroundingMetadata", candidate.GroundingMetadata.HasValue); + + var grounding = candidate.GroundingMetadata.Value; + + AssertEq("WebSearchQueries count", grounding.WebSearchQueries.Count(), 1); + AssertEq("WebSearchQueries content", grounding.WebSearchQueries.First(), + "current weather in London"); + + Assert("SearchEntryPoint should not be null", grounding.SearchEntryPoint.HasValue); + Assert("SearchEntryPoint content should not be empty", !string.IsNullOrEmpty(grounding.SearchEntryPoint.Value.RenderedContent)); + + AssertEq("GroundingChunks count", grounding.GroundingChunks.Count(), 2); + var firstChunk = grounding.GroundingChunks.First(); + Assert("GroundingChunk.Web should not be null", firstChunk.Web.HasValue); + var webChunk = firstChunk.Web.Value; + AssertEq("WebGroundingChunk.Title", webChunk.Title, "accuweather.com"); + Assert("WebGroundingChunk.Uri should not be null", webChunk.Uri != null); + Assert("WebGroundingChunk.Domain should be null or empty", string.IsNullOrEmpty(webChunk.Domain)); + + AssertEq("GroundingSupports count", grounding.GroundingSupports.Count(), 3); + var firstSupport = grounding.GroundingSupports.First(); + var segment = firstSupport.Segment; + AssertEq("Segment.Text", segment.Text, "The current weather in London, United Kingdom is cloudy."); + AssertEq("Segment.StartIndex", segment.StartIndex, 0); + AssertEq("Segment.PartIndex", segment.PartIndex, 0); + AssertEq("Segment.EndIndex", segment.EndIndex, 56); + AssertEq("GroundingChunkIndices count", firstSupport.GroundingChunkIndices.Count(), 1); + AssertEq("GroundingChunkIndices content", firstSupport.GroundingChunkIndices.First(), 0); + } + + // Test that parsing a Google AI response with empty GroundingChunks works. + // https://github.com/FirebaseExtended/vertexai-sdk-test-data/blob/main/mock-responses/googleai/unary-success-google-search-grounding-empty-grounding-chunks.json + async Task InternalTestGoogleAIGroundingEmptyChunks() { + Dictionary json = await GetGoogleAIJsonTestData("unary-success-google-search-grounding-empty-grounding-chunks.json"); + GenerateContentResponse response = GenerateContentResponse.FromJson(json, FirebaseAI.Backend.InternalProvider.GoogleAI); + + Assert("Response missing candidates.", response.Candidates.Any()); + var candidate = response.Candidates.First(); + Assert("Candidate should have GroundingMetadata", candidate.GroundingMetadata.HasValue); + + var grounding = candidate.GroundingMetadata.Value; + AssertEq("WebSearchQueries count", grounding.WebSearchQueries.Count(), 1); + AssertEq("GroundingChunks count", grounding.GroundingChunks.Count(), 2); + Assert("First GroundingChunk.Web should be null", !grounding.GroundingChunks.ElementAt(0).Web.HasValue); + Assert("Second GroundingChunk.Web should be null", !grounding.GroundingChunks.ElementAt(1).Web.HasValue); + + AssertEq("GroundingSupports count", grounding.GroundingSupports.Count(), 1); + var support = grounding.GroundingSupports.First(); + AssertEq( + "Segment.Text", + support.Segment.Text, + "There is a 0% chance of rain and the humidity is around 41%."); + } + + // Test parsing an empty GroundingMetadata object. + async Task InternalTestGroundingMetadata_Empty() { + var json = new Dictionary(); + var grounding = GroundingMetadata.FromJson(json); + + Assert("WebSearchQueries should be empty", !grounding.WebSearchQueries.Any()); + Assert("GroundingChunks should be empty", !grounding.GroundingChunks.Any()); + Assert("GroundingSupports should be empty", !grounding.GroundingSupports.Any()); + Assert("SearchEntryPoint should be null", !grounding.SearchEntryPoint.HasValue); + } + + // Test parsing an empty Segment object. + async Task InternalTestSegment_Empty() { + var json = new Dictionary(); + var segment = Segment.FromJson(json); + + AssertEq("PartIndex should default to 0", segment.PartIndex, 0); + AssertEq("StartIndex should default to 0", segment.StartIndex, 0); + AssertEq("EndIndex should default to 0", segment.EndIndex, 0); + Assert("Text should be empty", string.IsNullOrEmpty(segment.Text)); + } + // Test that parsing a count token response works. async Task InternalTestCountTokenResponse() { Dictionary json = await GetVertexJsonTestData("unary-success-detailed-token-response.json"); @@ -1101,7 +1251,7 @@ async Task InternalTestGoogleAICitations() { // Validate Text Part (check start and end) string expectedStart = "Okay, let's break down quantum mechanics."; - string expectedEnd = "area of physics!"; + string expectedEnd = "It's a challenging but fascinating area of physics!"; Assert("Candidate count", response.Candidates.Count() == 1); Candidate candidate = response.Candidates.First(); AssertEq("Content role", candidate.Content.Role, "model"); From 938003ac7bb020e05d94d66208b8c737f70338b9 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 9 Jul 2025 10:20:12 -0400 Subject: [PATCH 2/2] Review fixes --- firebaseai/src/GenerateContentResponse.cs | 11 ++++++++--- .../Sample/FirebaseAI/UIHandlerAutomated.cs | 18 +++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/firebaseai/src/GenerateContentResponse.cs b/firebaseai/src/GenerateContentResponse.cs index 13133d25..8196bc9d 100644 --- a/firebaseai/src/GenerateContentResponse.cs +++ b/firebaseai/src/GenerateContentResponse.cs @@ -310,7 +310,7 @@ public readonly struct WebGroundingChunk { /// /// The URI of the retrieved web page. /// - public string Uri { get; } + public System.Uri Uri { get; } /// /// The title of the retrieved web page. /// @@ -322,15 +322,20 @@ public readonly struct WebGroundingChunk { /// public string Domain { get; } - private WebGroundingChunk(string uri, string title, string domain) { + private WebGroundingChunk(System.Uri uri, string title, string domain) { Uri = uri; Title = title; Domain = domain; } internal static WebGroundingChunk FromJson(Dictionary jsonDict) { + Uri uri = null; + if (jsonDict.TryParseValue("uri", out string uriString)) { + uri = new Uri(uriString); + } + return new WebGroundingChunk( - jsonDict.ParseValue("uri"), + uri, jsonDict.ParseValue("title"), jsonDict.ParseValue("domain") ); diff --git a/firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs b/firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs index 32777a02..616aa718 100644 --- a/firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs +++ b/firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs @@ -465,7 +465,7 @@ async Task TestAnyOfSchemaResponse(Backend backend) { Schema.AnyOf(new[] { Schema.Int(), Schema.String() }), minItems: 2, maxItems: 6))); - + var response = await model.GenerateContentAsync( "Hello, I am testing setting the response schema with an array, cause you give me some random values."); @@ -1082,14 +1082,14 @@ async Task InternalTestVertexAIGrounding() { var grounding = candidate.GroundingMetadata.Value; - Assert("WebSearchQueries should not be null", grounding.WebSearchQueries != null); + Assert("WebSearchQueries should not be empty", grounding.WebSearchQueries.Any()); Assert("SearchEntryPoint should not be null", grounding.SearchEntryPoint.HasValue); - Assert("GroundingChunks should not be null", grounding.GroundingChunks != null); + Assert("GroundingChunks should not be empty", grounding.GroundingChunks.Any()); var chunk = grounding.GroundingChunks.First(); Assert("GroundingChunk.Web should not be null", chunk.Web.HasValue); - Assert("GroundingSupports should not be null", grounding.GroundingSupports != null); + Assert("GroundingSupports should not be empty", grounding.GroundingSupports.Any()); var support = grounding.GroundingSupports.First(); - Assert("GroundingChunkIndices should not be null", support.GroundingChunkIndices != null); + Assert("GroundingChunkIndices should not be empty", support.GroundingChunkIndices.Any()); } // Test that parsing a Google AI response with GroundingMetadata works. @@ -1155,7 +1155,7 @@ async Task InternalTestGoogleAIGroundingEmptyChunks() { } // Test parsing an empty GroundingMetadata object. - async Task InternalTestGroundingMetadata_Empty() { + Task InternalTestGroundingMetadata_Empty() { var json = new Dictionary(); var grounding = GroundingMetadata.FromJson(json); @@ -1163,10 +1163,12 @@ async Task InternalTestGroundingMetadata_Empty() { Assert("GroundingChunks should be empty", !grounding.GroundingChunks.Any()); Assert("GroundingSupports should be empty", !grounding.GroundingSupports.Any()); Assert("SearchEntryPoint should be null", !grounding.SearchEntryPoint.HasValue); + + return Task.CompletedTask; } // Test parsing an empty Segment object. - async Task InternalTestSegment_Empty() { + Task InternalTestSegment_Empty() { var json = new Dictionary(); var segment = Segment.FromJson(json); @@ -1174,6 +1176,8 @@ async Task InternalTestSegment_Empty() { AssertEq("StartIndex should default to 0", segment.StartIndex, 0); AssertEq("EndIndex should default to 0", segment.EndIndex, 0); Assert("Text should be empty", string.IsNullOrEmpty(segment.Text)); + + return Task.CompletedTask; } // Test that parsing a count token response works.