Skip to content

Commit a2db3ae

Browse files
authored
[FirebaseAI] Add support for Grounding with Google Search (#15014)
1 parent 8bf009c commit a2db3ae

File tree

8 files changed

+615
-1
lines changed

8 files changed

+615
-1
lines changed

FirebaseAI/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Unreleased
2+
- [added] Added support for Grounding with Google Search. (#15014)
23
- [removed] Removed `CountTokensResponse.totalBillableCharacters` which was
34
deprecated in 11.15.0. Use `totalTokens` instead. (#15056)
45

FirebaseAI/Sources/GenerateContentResponse.swift

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,16 @@ public struct Candidate: Sendable {
136136
/// Cited works in the model's response content, if it exists.
137137
public let citationMetadata: CitationMetadata?
138138

139+
public let groundingMetadata: GroundingMetadata?
140+
139141
/// Initializer for SwiftUI previews or tests.
140142
public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?,
141-
citationMetadata: CitationMetadata?) {
143+
citationMetadata: CitationMetadata?, groundingMetadata: GroundingMetadata? = nil) {
142144
self.content = content
143145
self.safetyRatings = safetyRatings
144146
self.finishReason = finishReason
145147
self.citationMetadata = citationMetadata
148+
self.groundingMetadata = groundingMetadata
146149
}
147150
}
148151

@@ -299,6 +302,110 @@ public struct PromptFeedback: Sendable {
299302
}
300303
}
301304

305+
/// Metadata returned to the client when grounding is enabled.
306+
///
307+
/// > Important: If using Grounding with Google Search, you are required to comply with the
308+
/// "Grounding with Google Search" usage requirements for your chosen API provider:
309+
/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search)
310+
/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms)
311+
/// section within the Service Specific Terms).
312+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
313+
public struct GroundingMetadata: Sendable {
314+
/// A list of web search queries that the model performed to gather the grounding information.
315+
/// These can be used to allow users to explore the search results themselves.
316+
public let webSearchQueries: [String]
317+
/// A list of ``GroundingChunk`` structs. Each chunk represents a piece of retrieved content
318+
/// (e.g., from a web page) that the model used to ground its response.
319+
public let groundingChunks: [GroundingChunk]
320+
/// A list of ``GroundingSupport`` structs. Each object details how specific segments of the
321+
/// model's response are supported by the `groundingChunks`.
322+
public let groundingSupports: [GroundingSupport]
323+
/// Google Search entry point for web searches.
324+
/// This contains an HTML/CSS snippet that **must** be embedded in an app to display a Google
325+
/// Search entry point for follow-up web searches related to the model's "Grounded Response".
326+
public let searchEntryPoint: SearchEntryPoint?
327+
328+
/// A struct representing the Google Search entry point.
329+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
330+
public struct SearchEntryPoint: Sendable {
331+
/// An HTML/CSS snippet that can be embedded in your app.
332+
///
333+
/// To ensure proper rendering, it's recommended to display this content within a `WKWebView`.
334+
public let renderedContent: String
335+
}
336+
337+
/// Represents a chunk of retrieved data that supports a claim in the model's response. This is
338+
/// part of the grounding information provided when grounding is enabled.
339+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
340+
public struct GroundingChunk: Sendable {
341+
/// Contains details if the grounding chunk is from a web source.
342+
public let web: WebGroundingChunk?
343+
}
344+
345+
/// A grounding chunk sourced from the web.
346+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
347+
public struct WebGroundingChunk: Sendable {
348+
/// The URI of the retrieved web page.
349+
public let uri: String?
350+
/// The title of the retrieved web page.
351+
public let title: String?
352+
/// The domain of the original URI from which the content was retrieved.
353+
///
354+
/// This field is only populated when using the Vertex AI Gemini API.
355+
public let domain: String?
356+
}
357+
358+
/// Provides information about how a specific segment of the model's response is supported by the
359+
/// retrieved grounding chunks.
360+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
361+
public struct GroundingSupport: Sendable {
362+
/// Specifies the segment of the model's response content that this grounding support pertains
363+
/// to.
364+
public let segment: Segment
365+
366+
/// A list of indices that refer to specific ``GroundingChunk`` structs within the
367+
/// ``GroundingMetadata/groundingChunks`` array. These referenced chunks are the sources that
368+
/// support the claim made in the associated `segment` of the response. For example, an array
369+
/// `[1, 3, 4]`
370+
/// means that `groundingChunks[1]`, `groundingChunks[3]`, `groundingChunks[4]` are the
371+
/// retrieved content supporting this part of the response.
372+
public let groundingChunkIndices: [Int]
373+
374+
struct Internal {
375+
let segment: Segment?
376+
let groundingChunkIndices: [Int]
377+
378+
func toPublic() -> GroundingSupport? {
379+
if segment == nil {
380+
return nil
381+
}
382+
return GroundingSupport(
383+
segment: segment!,
384+
groundingChunkIndices: groundingChunkIndices
385+
)
386+
}
387+
}
388+
}
389+
}
390+
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+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
394+
public struct Segment: Sendable {
395+
/// The zero-based index of the ``Part`` object within the `parts` array of its parent
396+
/// ``ModelContent`` object. This identifies which part of the content the segment belongs to.
397+
public let partIndex: Int
398+
/// The zero-based start index of the segment within the specified ``Part``, measured in UTF-8
399+
/// bytes. This offset is inclusive, starting from 0 at the beginning of the part's content.
400+
public let startIndex: Int
401+
/// The zero-based end index of the segment within the specified ``Part``, measured in UTF-8
402+
/// bytes. This offset is exclusive, meaning the character at this index is not included in the
403+
/// segment.
404+
public let endIndex: Int
405+
/// The text corresponding to the segment from the response.
406+
public let text: String
407+
}
408+
302409
// MARK: - Codable Conformances
303410

304411
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
@@ -369,6 +476,7 @@ extension Candidate: Decodable {
369476
case safetyRatings
370477
case finishReason
371478
case citationMetadata
479+
case groundingMetadata
372480
}
373481

374482
/// Initializes a response from a decoder. Used for decoding server responses; not for public
@@ -414,6 +522,11 @@ extension Candidate: Decodable {
414522
CitationMetadata.self,
415523
forKey: .citationMetadata
416524
)
525+
526+
groundingMetadata = try container.decodeIfPresent(
527+
GroundingMetadata.self,
528+
forKey: .groundingMetadata
529+
)
417530
}
418531
}
419532

@@ -513,3 +626,74 @@ extension PromptFeedback: Decodable {
513626
}
514627
}
515628
}
629+
630+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
631+
extension GroundingMetadata: Decodable {
632+
enum CodingKeys: String, CodingKey {
633+
case webSearchQueries
634+
case groundingChunks
635+
case groundingSupports
636+
case searchEntryPoint
637+
}
638+
639+
public init(from decoder: Decoder) throws {
640+
let container = try decoder.container(keyedBy: CodingKeys.self)
641+
webSearchQueries = try container.decodeIfPresent([String].self, forKey: .webSearchQueries) ?? []
642+
groundingChunks = try container.decodeIfPresent(
643+
[GroundingChunk].self,
644+
forKey: .groundingChunks
645+
) ?? []
646+
groundingSupports = try container.decodeIfPresent(
647+
[GroundingSupport.Internal].self,
648+
forKey: .groundingSupports
649+
)?.compactMap { $0.toPublic() } ?? []
650+
searchEntryPoint = try container.decodeIfPresent(
651+
SearchEntryPoint.self,
652+
forKey: .searchEntryPoint
653+
)
654+
}
655+
}
656+
657+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
658+
extension GroundingMetadata.SearchEntryPoint: Decodable {}
659+
660+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
661+
extension GroundingMetadata.GroundingChunk: Decodable {}
662+
663+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
664+
extension GroundingMetadata.WebGroundingChunk: Decodable {}
665+
666+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
667+
extension GroundingMetadata.GroundingSupport.Internal: Decodable {
668+
enum CodingKeys: String, CodingKey {
669+
case segment
670+
case groundingChunkIndices
671+
}
672+
673+
public init(from decoder: Decoder) throws {
674+
let container = try decoder.container(keyedBy: CodingKeys.self)
675+
segment = try container.decodeIfPresent(Segment.self, forKey: .segment)
676+
groundingChunkIndices = try container.decodeIfPresent(
677+
[Int].self,
678+
forKey: .groundingChunkIndices
679+
) ?? []
680+
}
681+
}
682+
683+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
684+
extension Segment: Decodable {
685+
enum CodingKeys: String, CodingKey {
686+
case partIndex
687+
case startIndex
688+
case endIndex
689+
case text
690+
}
691+
692+
public init(from decoder: Decoder) throws {
693+
let container = try decoder.container(keyedBy: CodingKeys.self)
694+
partIndex = try container.decodeIfPresent(Int.self, forKey: .partIndex) ?? 0
695+
startIndex = try container.decodeIfPresent(Int.self, forKey: .startIndex) ?? 0
696+
endIndex = try container.decodeIfPresent(Int.self, forKey: .endIndex) ?? 0
697+
text = try container.decodeIfPresent(String.self, forKey: .text) ?? ""
698+
}
699+
}

FirebaseAI/Sources/FunctionCalling.swift renamed to FirebaseAI/Sources/Tool.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ public struct FunctionDeclaration: Sendable {
5050
}
5151
}
5252

53+
/// A tool that allows the generative model to connect to Google Search to access and incorporate
54+
/// up-to-date information from the web into its responses.
55+
///
56+
/// > Important: When using this feature, you are required to comply with the
57+
/// "Grounding with Google Search" usage requirements for your chosen API provider:
58+
/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search)
59+
/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms)
60+
/// section within the Service Specific Terms).
61+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
62+
public struct GoogleSearch: Sendable {
63+
public init() {}
64+
}
65+
5366
/// A helper tool that the model may use when generating responses.
5467
///
5568
/// A `Tool` is a piece of code that enables the system to interact with external systems to perform
@@ -58,9 +71,17 @@ public struct FunctionDeclaration: Sendable {
5871
public struct Tool: Sendable {
5972
/// A list of `FunctionDeclarations` available to the model.
6073
let functionDeclarations: [FunctionDeclaration]?
74+
/// Specifies the Google Search configuration.
75+
let googleSearch: GoogleSearch?
6176

6277
init(functionDeclarations: [FunctionDeclaration]?) {
6378
self.functionDeclarations = functionDeclarations
79+
googleSearch = nil
80+
}
81+
82+
init(googleSearch: GoogleSearch) {
83+
self.googleSearch = googleSearch
84+
functionDeclarations = nil
6485
}
6586

6687
/// Creates a tool that allows the model to perform function calling.
@@ -85,6 +106,26 @@ public struct Tool: Sendable {
85106
public static func functionDeclarations(_ functionDeclarations: [FunctionDeclaration]) -> Tool {
86107
return self.init(functionDeclarations: functionDeclarations)
87108
}
109+
110+
/// Creates a tool that allows the model to use Grounding with Google Search.
111+
///
112+
/// Grounding with Google Search can be used to allow the model to connect to Google Search to
113+
/// access and incorporate up-to-date information from the web into it's responses.
114+
///
115+
/// > Important: When using this feature, you are required to comply with the
116+
/// "Grounding with Google Search" usage requirements for your chosen API provider:
117+
/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search)
118+
/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms)
119+
/// section within the Service Specific Terms).
120+
///
121+
/// - Parameters:
122+
/// - googleSearch: An empty ``GoogleSearch`` object. The presence of this object in the list
123+
/// of tools enables the model to use Google Search.
124+
///
125+
/// - Returns: A `Tool` configured for Google Search.
126+
public static func googleSearch(_ googleSearch: GoogleSearch = GoogleSearch()) -> Tool {
127+
return self.init(googleSearch: googleSearch)
128+
}
88129
}
89130

90131
/// Configuration for specifying function calling behavior.
@@ -170,5 +211,8 @@ extension FunctionCallingConfig: Encodable {}
170211
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
171212
extension FunctionCallingConfig.Mode: Encodable {}
172213

214+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
215+
extension GoogleSearch: Encodable {}
216+
173217
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
174218
extension ToolConfig: Encodable {}

FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,45 @@ struct GenerateContentIntegrationTests {
247247
#endif // canImport(UIKit)
248248
}
249249

250+
@Test(
251+
"generateContent with Google Search returns grounding metadata",
252+
arguments: InstanceConfig.allConfigs
253+
)
254+
func generateContent_withGoogleSearch_succeeds(_ config: InstanceConfig) async throws {
255+
let model = FirebaseAI.componentInstance(config).generativeModel(
256+
modelName: ModelNames.gemini2Flash,
257+
tools: [.googleSearch()]
258+
)
259+
let prompt = "What is the weather in Toronto today?"
260+
261+
let response = try await model.generateContent(prompt)
262+
263+
let candidate = try #require(response.candidates.first)
264+
let groundingMetadata = try #require(candidate.groundingMetadata)
265+
let searchEntrypoint = try #require(groundingMetadata.searchEntryPoint)
266+
267+
#expect(!groundingMetadata.webSearchQueries.isEmpty)
268+
#expect(!searchEntrypoint.renderedContent.isEmpty)
269+
#expect(!groundingMetadata.groundingChunks.isEmpty)
270+
#expect(!groundingMetadata.groundingSupports.isEmpty)
271+
272+
for chunk in groundingMetadata.groundingChunks {
273+
#expect(chunk.web != nil)
274+
}
275+
276+
for support in groundingMetadata.groundingSupports {
277+
let segment = support.segment
278+
#expect(segment.endIndex > segment.startIndex)
279+
#expect(!segment.text.isEmpty)
280+
#expect(!support.groundingChunkIndices.isEmpty)
281+
282+
// Ensure indices point to valid chunks
283+
for index in support.groundingChunkIndices {
284+
#expect(index < groundingMetadata.groundingChunks.count)
285+
}
286+
}
287+
}
288+
250289
// MARK: Streaming Tests
251290

252291
@Test(arguments: [

0 commit comments

Comments
 (0)