Skip to content
Merged
3 changes: 3 additions & 0 deletions FirebaseAI/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Unreleased
- [added] Added support for Grounding with Google Search. (#15014)

# 11.15.0
- [fixed] Fixed `Sendable` warnings introduced in the Xcode 26 beta. (#14947)
- [added] Added support for setting `title` in string, number and array `Schema`
Expand Down
214 changes: 213 additions & 1 deletion FirebaseAI/Sources/GenerateContentResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,16 @@ public struct Candidate: Sendable {
/// Cited works in the model's response content, if it exists.
public let citationMetadata: CitationMetadata?

public let groundingMetadata: GroundingMetadata?

/// Initializer for SwiftUI previews or tests.
public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?,
citationMetadata: CitationMetadata?) {
citationMetadata: CitationMetadata?, groundingMetadata: GroundingMetadata? = nil) {
self.content = content
self.safetyRatings = safetyRatings
self.finishReason = finishReason
self.citationMetadata = citationMetadata
self.groundingMetadata = groundingMetadata
}
}

Expand Down Expand Up @@ -299,6 +302,138 @@ public struct PromptFeedback: Sendable {
}
}

/// Metadata returned to the client when grounding is enabled.
///
/// > Important: If using Grounding with Google Search, you are required to comply with the [Service
/// Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with Google
/// Search*.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct GroundingMetadata: Sendable {
/// 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.
///
/// > Important: If using Grounding with Google Search, you are required to comply with the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with
/// Google Search*.
public let webSearchQueries: [String]
/// 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.
///
/// > Important: If using Grounding with Google Search, you are required to comply with the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with
/// Google Search*.
public let groundingChunks: [GroundingChunk]
/// A list of ``GroundingSupport`` structs. Each object details how specific segments of the
/// model's response are supported by the `groundingChunks`.
///
/// > Important: If using Grounding with Google Search, you are required to comply with the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with
/// Google Search*.
public let groundingSupports: [GroundingSupport]
/// 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".
///
/// > Important: If using Grounding with Google Search, you are required to comply with the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with
/// Google Search*.
public let searchEntryPoint: SearchEntryPoint?

/// A struct representing the Google Search entry point.
///
/// > Important: If using Grounding with Google Search, you are required to comply with the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with
/// Google Search*.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct SearchEntryPoint: Sendable {
/// An HTML/CSS snippet that can be embedded in your app. The snippet is designed to avoid
/// undesired interaction with the rest of the page's CSS.
///
/// To ensure proper rendering, it's recommended to display this content within a `WKWebView`.
public let renderedContent: String
}

/// 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.
///
/// > Important: If using Grounding with Google Search, you are required to comply with the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with
/// Google Search*.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct GroundingChunk: Sendable {
/// Contains details if the grounding chunk is from a web source.
public let web: WebGroundingChunk?
}

/// A grounding chunk sourced from the web.
///
/// > Important: If using Grounding with Google Search, you are required to comply with the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with
/// Google Search*.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct WebGroundingChunk: Sendable {
/// The URI of the retrieved web page.
public let uri: String?
/// The title of the retrieved web page.
public let title: String?
/// The domain of the original URI from which the content was retrieved (e.g., `example.com`).
///
/// This field is only populated when using the Vertex AI Gemini API.
public let domain: String?
}

/// Provides information about how a specific segment of the model's response is supported by the
/// retrieved grounding chunks.
///
/// > Important: If using Grounding with Google Search, you are required to comply with the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms) for *Grounding with
/// Google Search*.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct GroundingSupport: Sendable {
/// Specifies the segment of the model's response content that this grounding support pertains
/// to.
public let segment: Segment

/// 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.
public let groundingChunkIndices: [Int]

struct Internal {
let segment: Segment?
let groundingChunkIndices: [Int]

func toPublic() -> GroundingSupport? {
if segment == nil {
return nil
}
return GroundingSupport(
segment: segment!,
groundingChunkIndices: groundingChunkIndices
)
}
}
}
}

/// Represents a specific segment within a ``ModelContent`` struct, often used to pinpoint the
/// exact location of text or data that grounding information refers to.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct Segment: Sendable {
/// 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 let partIndex: Int
/// 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 let startIndex: Int
/// The zero-based end index of the segment within the specified ``Part``, measured in UTF-8
/// bytes. This offset is exclusive.
public let endIndex: Int
/// The text content of the segment.
public let text: String
}

// MARK: - Codable Conformances

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
Expand Down Expand Up @@ -369,6 +504,7 @@ extension Candidate: Decodable {
case safetyRatings
case finishReason
case citationMetadata
case groundingMetadata
}

/// Initializes a response from a decoder. Used for decoding server responses; not for public
Expand Down Expand Up @@ -414,6 +550,11 @@ extension Candidate: Decodable {
CitationMetadata.self,
forKey: .citationMetadata
)

groundingMetadata = try container.decodeIfPresent(
GroundingMetadata.self,
forKey: .groundingMetadata
)
}
}

Expand Down Expand Up @@ -513,3 +654,74 @@ extension PromptFeedback: Decodable {
}
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension GroundingMetadata: Decodable {
enum CodingKeys: String, CodingKey {
case webSearchQueries
case groundingChunks
case groundingSupports
case searchEntryPoint
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
webSearchQueries = try container.decodeIfPresent([String].self, forKey: .webSearchQueries) ?? []
groundingChunks = try container.decodeIfPresent(
[GroundingChunk].self,
forKey: .groundingChunks
) ?? []
groundingSupports = try container.decodeIfPresent(
[GroundingSupport.Internal].self,
forKey: .groundingSupports
)?.compactMap { $0.toPublic() } ?? []
searchEntryPoint = try container.decodeIfPresent(
SearchEntryPoint.self,
forKey: .searchEntryPoint
)
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension GroundingMetadata.SearchEntryPoint: Decodable {}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension GroundingMetadata.GroundingChunk: Decodable {}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension GroundingMetadata.WebGroundingChunk: Decodable {}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension GroundingMetadata.GroundingSupport.Internal: Decodable {
enum CodingKeys: String, CodingKey {
case segment
case groundingChunkIndices
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
segment = try container.decodeIfPresent(Segment.self, forKey: .segment)
groundingChunkIndices = try container.decodeIfPresent(
[Int].self,
forKey: .groundingChunkIndices
) ?? []
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Segment: Decodable {
enum CodingKeys: String, CodingKey {
case partIndex
case startIndex
case endIndex
case text
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
partIndex = try container.decodeIfPresent(Int.self, forKey: .partIndex) ?? 0
startIndex = try container.decodeIfPresent(Int.self, forKey: .startIndex) ?? 0
endIndex = try container.decodeIfPresent(Int.self, forKey: .endIndex) ?? 0
text = try container.decodeIfPresent(String.self, forKey: .text) ?? ""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public struct FunctionDeclaration: Sendable {
}
}

/// 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 this tool is used, the model's responses may include "Grounded Results" which
/// are subject
/// to the Grounding with Google Search terms outlined in the
/// [Service Specific Terms](https://cloud.google.com/terms/service-terms).
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct GoogleSearch: Sendable {
public init() {}
}

/// A helper tool that the model may use when generating responses.
///
/// A `Tool` is a piece of code that enables the system to interact with external systems to perform
Expand All @@ -58,9 +70,16 @@ public struct FunctionDeclaration: Sendable {
public struct Tool: Sendable {
/// A list of `FunctionDeclarations` available to the model.
let functionDeclarations: [FunctionDeclaration]?
let googleSearch: GoogleSearch?

init(functionDeclarations: [FunctionDeclaration]?) {
self.functionDeclarations = functionDeclarations
googleSearch = nil
}

init(googleSearch: GoogleSearch) {
self.googleSearch = googleSearch
functionDeclarations = nil
}

/// Creates a tool that allows the model to perform function calling.
Expand All @@ -85,6 +104,24 @@ public struct Tool: Sendable {
public static func functionDeclarations(_ functionDeclarations: [FunctionDeclaration]) -> Tool {
return self.init(functionDeclarations: functionDeclarations)
}

/// Creates a tool that allows the model to use Grounding with Google Search.
///
/// Grounding with Google Search can be used to allow the model to connect to Google Search to
/// access and incorporate up-to-date information from the web into it's responses.
///
/// When this tool is used, the model's responses may include "Grounded Results" which are subject
/// to the Grounding with Google Search terms outlined in the [Service Specific
/// Terms](https://cloud.google.com/terms/service-terms).
///
/// - Parameters:
/// - googleSearch: An empty ``GoogleSearch`` object. The presence of this object in the list
/// of tools enables the model to use Google Search.
///
/// - Returns: A `Tool` configured for Google Search.
public static func googleSearch(_ googleSearch: GoogleSearch = GoogleSearch()) -> Tool {
return self.init(googleSearch: googleSearch)
}
}

/// Configuration for specifying function calling behavior.
Expand Down Expand Up @@ -170,5 +207,8 @@ extension FunctionCallingConfig: Encodable {}
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension FunctionCallingConfig.Mode: Encodable {}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension GoogleSearch: Encodable {}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension ToolConfig: Encodable {}
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,45 @@
#endif // canImport(UIKit)
}

@Test(
"generateContent with Google Search returns grounding metadata",
arguments: InstanceConfig.allConfigs
)
func generateContent_withGoogleSearch_succeeds(_ config: InstanceConfig) async throws {
let model = FirebaseAI.componentInstance(config).generativeModel(
modelName: ModelNames.gemini2Flash,
tools: [.googleSearch()]
)
let prompt = "What is the weather in Toronto today?"

let response = try await model.generateContent(prompt)

let candidate = try #require(response.candidates.first)
let groundingMetadata = try #require(candidate.groundingMetadata)
let searchEntrypoint = try #require(groundingMetadata.searchEntryPoint)

#expect(!groundingMetadata.webSearchQueries.isEmpty)
#expect(searchEntrypoint.renderedContent != nil)

Check warning on line 268 in FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

View workflow job for this annotation

GitHub Actions / testapp-integration (iOS, macos-15)

comparing non-optional value of type 'String' to 'nil' always returns true

Check warning on line 268 in FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

View workflow job for this annotation

GitHub Actions / testapp-integration (iOS, macos-15)

comparing non-optional value of type 'String' to 'nil' always returns true

Check warning on line 268 in FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

View workflow job for this annotation

GitHub Actions / testapp-integration (iOS, macos-15)

comparing non-optional value of type 'String' to 'nil' always returns true
#expect(!groundingMetadata.groundingChunks.isEmpty)
#expect(!groundingMetadata.groundingSupports.isEmpty)

for chunk in groundingMetadata.groundingChunks {
#expect(chunk.web != nil)
}

for support in groundingMetadata.groundingSupports {
let segment = try #require(support.segment)

Check warning on line 277 in FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

View workflow job for this annotation

GitHub Actions / testapp-integration (iOS, macos-15)

'#require(_:_:)' is redundant because 'support.segment' never equals 'nil' (from macro 'require')

Check warning on line 277 in FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

View workflow job for this annotation

GitHub Actions / testapp-integration (iOS, macos-15)

'#require(_:_:)' is redundant because 'support.segment' never equals 'nil' (from macro 'require')

Check warning on line 277 in FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

View workflow job for this annotation

GitHub Actions / testapp-integration (iOS, macos-15)

'#require(_:_:)' is redundant because 'support.segment' never equals 'nil' (from macro 'require')
#expect(segment.endIndex > segment.startIndex)
#expect(!segment.text.isEmpty)
#expect(!support.groundingChunkIndices.isEmpty)

// Ensure indices point to valid chunks
for index in support.groundingChunkIndices {
#expect(index < groundingMetadata.groundingChunks.count)
}
}
}

// MARK: Streaming Tests

@Test(arguments: [
Expand Down
Loading
Loading