Skip to content

Commit 4664f6f

Browse files
committed
FirebaseAI Grounding with Google Search
1 parent ec4b353 commit 4664f6f

File tree

9 files changed

+595
-37
lines changed

9 files changed

+595
-37
lines changed

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt

Lines changed: 207 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import com.google.firebase.ai.common.util.FirstOrdinalSerializer
2222
import java.util.Calendar
2323
import kotlinx.serialization.ExperimentalSerializationApi
2424
import kotlinx.serialization.KSerializer
25-
import kotlinx.serialization.SerialName
2625
import kotlinx.serialization.Serializable
2726
import kotlinx.serialization.json.JsonNames
2827

@@ -39,7 +38,8 @@ internal constructor(
3938
public val content: Content,
4039
public val safetyRatings: List<SafetyRating>,
4140
public val citationMetadata: CitationMetadata?,
42-
public val finishReason: FinishReason?
41+
public val finishReason: FinishReason?,
42+
public val groundingMetadata: GroundingMetadata?
4343
) {
4444

4545
@Serializable
@@ -48,48 +48,22 @@ internal constructor(
4848
val finishReason: FinishReason.Internal? = null,
4949
val safetyRatings: List<SafetyRating.Internal>? = null,
5050
val citationMetadata: CitationMetadata.Internal? = null,
51-
val groundingMetadata: GroundingMetadata? = null,
51+
val groundingMetadata: GroundingMetadata.Internal? = null
5252
) {
5353
internal fun toPublic(): Candidate {
5454
val safetyRatings = safetyRatings?.mapNotNull { it.toPublic() }.orEmpty()
5555
val citations = citationMetadata?.toPublic()
5656
val finishReason = finishReason?.toPublic()
57+
val groundingMetadata = groundingMetadata?.toPublic()
5758

5859
return Candidate(
5960
this.content?.toPublic() ?: content("model") {},
6061
safetyRatings,
6162
citations,
62-
finishReason
63+
finishReason,
64+
groundingMetadata
6365
)
6466
}
65-
66-
@Serializable
67-
internal data class GroundingMetadata(
68-
@SerialName("web_search_queries") val webSearchQueries: List<String>?,
69-
@SerialName("search_entry_point") val searchEntryPoint: SearchEntryPoint?,
70-
@SerialName("retrieval_queries") val retrievalQueries: List<String>?,
71-
@SerialName("grounding_attribution") val groundingAttribution: List<GroundingAttribution>?,
72-
) {
73-
74-
@Serializable
75-
internal data class SearchEntryPoint(
76-
@SerialName("rendered_content") val renderedContent: String?,
77-
@SerialName("sdk_blob") val sdkBlob: String?,
78-
)
79-
80-
@Serializable
81-
internal data class GroundingAttribution(
82-
val segment: Segment,
83-
@SerialName("confidence_score") val confidenceScore: Float?,
84-
) {
85-
86-
@Serializable
87-
internal data class Segment(
88-
@SerialName("start_index") val startIndex: Int,
89-
@SerialName("end_index") val endIndex: Int,
90-
)
91-
}
92-
}
9367
}
9468
}
9569

@@ -252,7 +226,7 @@ public class FinishReason private constructor(public val name: String, public va
252226
@Serializable(Internal.Serializer::class)
253227
internal enum class Internal {
254228
UNKNOWN,
255-
@SerialName("FINISH_REASON_UNSPECIFIED") UNSPECIFIED,
229+
UNSPECIFIED,
256230
STOP,
257231
MAX_TOKENS,
258232
SAFETY,
@@ -317,3 +291,203 @@ public class FinishReason private constructor(public val name: String, public va
317291
public val MALFORMED_FUNCTION_CALL: FinishReason = FinishReason("MALFORMED_FUNCTION_CALL", 9)
318292
}
319293
}
294+
295+
/**
296+
* Metadata returned to the client when grounding is enabled.
297+
*
298+
* If using Grounding with Google Search, you are required to comply with the
299+
* [Service Specific Terms](https://cloud.google.com/terms/service-terms) for "Grounding with Google
300+
* Search".
301+
*
302+
* @property webSearchQueries A list of web search queries that the model performed to gather the
303+
* grounding information. These can be used to allow users to explore the search results themselves.
304+
* @property searchEntryPoint Google search entry point for web searches. This contains an HTML/CSS
305+
* snippet that **must** be embedded in an app to display a Google Search Entry point for follow-up
306+
* web searches related to the model's "Grounded Response". To ensure proper rendering, it's
307+
* recommended to display this content within a `WebView`.
308+
* @property groundingChunks A list of [GroundingChunk] objects. Each chunk represents a piece of
309+
* retrieved content (e.g. from a web page) that the model used to ground its response.
310+
* @property groundingSupports A list of [GroundingSupport] objects. Each object details how
311+
* specific segments of the model's response are supported by the `groundingChunks`.
312+
*/
313+
public class GroundingMetadata(
314+
public val webSearchQueries: List<String>?,
315+
public val searchEntryPoint: SearchEntryPoint?,
316+
public val retrievalQueries: List<String>?,
317+
@Deprecated("Use groundingChunks instead")
318+
public val groundingAttribution: List<GroundingAttribution>?,
319+
public val groundingChunks: List<GroundingChunk>,
320+
public val groundingSupports: List<GroundingSupport>,
321+
) {
322+
@Serializable
323+
internal data class Internal(
324+
val webSearchQueries: List<String>?,
325+
val searchEntryPoint: SearchEntryPoint.Internal?,
326+
val retrievalQueries: List<String>?,
327+
@Deprecated("Use groundingChunks instead")
328+
val groundingAttribution: List<GroundingAttribution.Internal>?,
329+
val groundingChunks: List<GroundingChunk.Internal>?,
330+
val groundingSupports: List<GroundingSupport.Internal>?,
331+
) {
332+
internal fun toPublic() =
333+
GroundingMetadata(
334+
webSearchQueries = webSearchQueries,
335+
searchEntryPoint = searchEntryPoint?.toPublic(),
336+
retrievalQueries = retrievalQueries,
337+
groundingAttribution = groundingAttribution?.map { it.toPublic() },
338+
groundingChunks = groundingChunks?.map { it.toPublic() }.orEmpty(),
339+
groundingSupports = groundingSupports?.map { it.toPublic() }.orEmpty()
340+
)
341+
}
342+
}
343+
344+
/**
345+
* A class representing the Google Search entry point.
346+
*
347+
* When using this feature, you are required to comply with the
348+
* [Service Specific Terms](https://cloud.google.com/terms/service-terms) for "Grounding with Google
349+
* Search".
350+
*
351+
* @property renderedContent An HTML/CSS snippet that can be embedded in your app. The snippet is
352+
* designed to avoid undesired interaction with the rest of the page's CSS. To ensure proper
353+
* rendering, it's recommended to display this content within a `WebView`.
354+
* @property sdkBlob A blob of data for the client SDK to render the search entry point.
355+
*/
356+
public class SearchEntryPoint(
357+
public val renderedContent: String?,
358+
public val sdkBlob: String?,
359+
) {
360+
@Serializable
361+
internal data class Internal(
362+
val renderedContent: String?,
363+
val sdkBlob: String?,
364+
) {
365+
internal fun toPublic() = SearchEntryPoint(renderedContent = renderedContent, sdkBlob = sdkBlob)
366+
}
367+
}
368+
369+
/**
370+
* Represents a chunk of retrieved data that supports a claim in the model's response. This is part
371+
* of the grounding information provided when grounding is enabled.
372+
*
373+
* When using this feature, you are required to comply with the
374+
* [Service Specific Terms](https://cloud.google.com/terms/service-terms) for "Grounding with Google
375+
* Search".
376+
*
377+
* @property web Contains details if the grounding chunk is from a web source.
378+
*/
379+
public class GroundingChunk(
380+
public val web: WebGroundingChunk?,
381+
) {
382+
@Serializable
383+
internal data class Internal(
384+
val web: WebGroundingChunk.Internal?,
385+
) {
386+
internal fun toPublic() = GroundingChunk(web = web?.toPublic())
387+
}
388+
}
389+
390+
/**
391+
* A grounding chunk from the web.
392+
*
393+
* When using this feature, you are required to comply with the
394+
* [Service Specific Terms](https://cloud.google.com/terms/service-terms) for "Grounding with Google
395+
* Search".
396+
*
397+
* @property uri The URI of the retrieved web page.
398+
* @property title The title of the retrieved web page.
399+
* @property domain The domain of the original URI from which the content was retrieved (e.g.,
400+
* `example.com`). This is only populated when using the Vertex AI Gemini API.
401+
*/
402+
public class WebGroundingChunk(
403+
public val uri: String?,
404+
public val title: String?,
405+
public val domain: String?
406+
) {
407+
@Serializable
408+
internal data class Internal(val uri: String?, val title: String?, val domain: String?) {
409+
internal fun toPublic() = WebGroundingChunk(uri = uri, title = title, domain = domain)
410+
}
411+
}
412+
413+
/**
414+
* Provides information about how a specific segment of the model's response is supported by the
415+
* retrieved grounding chunks.
416+
*
417+
* When using this feature, you are required to comply with the
418+
* [Service Specific Terms](https://cloud.google.com/terms/service-terms) for "Grounding with Google
419+
* Search".
420+
*
421+
* @property segment Specifies the segment of the model's response content that this grounding
422+
* support pertains to.
423+
* @property groundingChunkIndices A list of indices that refer to specific [GroundingChunk] objects
424+
* within the [GroundingMetadata.groundingChunks] array. These referenced chunks are the sources
425+
* that support the claim made in the associated `segment` of the response.
426+
*/
427+
public class GroundingSupport(
428+
public val segment: Segment?,
429+
public val groundingChunkIndices: List<Int>,
430+
) {
431+
@Serializable
432+
internal data class Internal(
433+
val segment: Segment.Internal?,
434+
val groundingChunkIndices: List<Int>?,
435+
) {
436+
internal fun toPublic() =
437+
GroundingSupport(
438+
segment = segment?.toPublic(),
439+
groundingChunkIndices = groundingChunkIndices.orEmpty(),
440+
)
441+
}
442+
}
443+
444+
@Deprecated("Use GroundingChunk instead")
445+
public class GroundingAttribution(
446+
public val segment: Segment,
447+
public val confidenceScore: Float?,
448+
) {
449+
@Deprecated("Use GroundingChunk instead")
450+
@Serializable
451+
internal data class Internal(
452+
val segment: Segment.Internal,
453+
val confidenceScore: Float?,
454+
) {
455+
internal fun toPublic() =
456+
GroundingAttribution(segment = segment.toPublic(), confidenceScore = confidenceScore)
457+
}
458+
}
459+
460+
/**
461+
* Represents a specific segment within a [Content] object, often used to pinpoint the exact
462+
* location of text or data that grounding information refers to.
463+
*
464+
* @property startIndex The zero-based start index of the segment within the specified [Part],
465+
* measured in UTF-8 bytes. This offset is inclusive.
466+
* @property endIndex The zero-based end index of the segment within the specified [Part], measured
467+
* in UTF-8 bytes. This offset is exclusive.
468+
* @property partIndex The zero-based index of the [Part] object within the `parts` array of its
469+
* parent [Content] object. This identifies which part of the content the segment belongs to.
470+
* @property text The text content of the segment.
471+
*/
472+
public class Segment(
473+
public val startIndex: Int,
474+
public val endIndex: Int,
475+
public val partIndex: Int,
476+
public val text: String,
477+
) {
478+
@Serializable
479+
internal data class Internal(
480+
val startIndex: Int?,
481+
val endIndex: Int?,
482+
val partIndex: Int?,
483+
val text: String?,
484+
) {
485+
internal fun toPublic() =
486+
Segment(
487+
startIndex = startIndex ?: 0,
488+
endIndex = endIndex ?: 0,
489+
partIndex = partIndex ?: 0,
490+
text = text ?: ""
491+
)
492+
}
493+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.google.firebase.ai.type
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* A tool that allows the generative model to connect to Google Search to access and incorporate
7+
* up-to-date information from the web into its responses.
8+
*
9+
* When this tool is used, the model's responses may include "Grounded Results" which are subject to
10+
* the Grounding with Google Search terms outlined in the
11+
* [Service Specific Terms](https://cloud.google.com/terms/service-terms).
12+
*/
13+
@Serializable public class GoogleSearch {}

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,19 @@ import kotlinx.serialization.json.JsonObject
2626
* @param functionDeclarations The set of functions that this tool allows the model access to
2727
*/
2828
public class Tool
29-
internal constructor(internal val functionDeclarations: List<FunctionDeclaration>?) {
30-
internal fun toInternal() = Internal(functionDeclarations?.map { it.toInternal() } ?: emptyList())
29+
internal constructor(
30+
internal val functionDeclarations: List<FunctionDeclaration>?,
31+
internal val googleSearch: GoogleSearch?
32+
) {
33+
internal fun toInternal() =
34+
Internal(
35+
functionDeclarations?.map { it.toInternal() } ?: emptyList(),
36+
googleSearch = this.googleSearch
37+
)
3138
@Serializable
3239
internal data class Internal(
3340
val functionDeclarations: List<FunctionDeclaration.Internal>? = null,
41+
val googleSearch: GoogleSearch? = null,
3442
// This is a json object because it is not possible to make a data class with no parameters.
3543
val codeExecution: JsonObject? = null,
3644
)
@@ -43,7 +51,26 @@ internal constructor(internal val functionDeclarations: List<FunctionDeclaration
4351
*/
4452
@JvmStatic
4553
public fun functionDeclarations(functionDeclarations: List<FunctionDeclaration>): Tool {
46-
return Tool(functionDeclarations)
54+
return Tool(functionDeclarations, null)
55+
}
56+
57+
/**
58+
* Creates a [Tool] instance that enables the model to use Grounding with Google Search.
59+
*
60+
* This allows the model to connect to Google Search to access and incorporate up-to-date
61+
* information from the web into its responses.
62+
*
63+
* When this tool is used, the model's responses may include "Grounded Results" which are
64+
* subject to the Grounding with Google Search terms outlined in the
65+
* [Service Specific Terms](https://cloud.google.com/terms/service-terms).
66+
*
67+
* @param googleSearch An empty [GoogleSearch] object. The presence of this object in the list
68+
* of tools enables the model to use Google Search.
69+
* @return a [Tool] configured for Google Search.
70+
*/
71+
@JvmStatic
72+
public fun googleSearch(googleSearch: GoogleSearch = GoogleSearch()): Tool {
73+
return Tool(null, googleSearch)
4774
}
4875
}
4976
}

firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import com.google.firebase.ai.type.ResponseStoppedException
2222
import com.google.firebase.ai.type.ServerException
2323
import com.google.firebase.ai.util.goldenDevAPIUnaryFile
2424
import io.kotest.assertions.throwables.shouldThrow
25+
import io.kotest.matchers.collections.shouldNotBeEmpty
26+
import io.kotest.matchers.nulls.shouldBeNull
27+
import io.kotest.matchers.nulls.shouldNotBeNull
2528
import io.kotest.matchers.should
2629
import io.kotest.matchers.shouldBe
2730
import io.kotest.matchers.shouldNotBe
@@ -95,4 +98,22 @@ internal class DevAPIUnarySnapshotTests {
9598
goldenDevAPIUnaryFile("unary-failure-unknown-model.json", HttpStatusCode.NotFound) {
9699
withTimeout(testTimeout) { shouldThrow<ServerException> { model.generateContent("prompt") } }
97100
}
101+
102+
// This test case can be removed once https://b.corp.google.com/issues/422779395 (internal) is
103+
// fixed.
104+
@Test
105+
fun `google search grounding empty grounding chunks`() =
106+
goldenDevAPIUnaryFile("unary-success-google-search-grounding-empty-grounding-chunks.json") {
107+
withTimeout(testTimeout) {
108+
val response = model.generateContent("prompt")
109+
110+
response.candidates.shouldNotBeEmpty()
111+
val candidate = response.candidates.first()
112+
val groundingMetadata = candidate.groundingMetadata
113+
groundingMetadata.shouldNotBeNull()
114+
115+
groundingMetadata.groundingChunks.shouldNotBeEmpty()
116+
groundingMetadata.groundingChunks.forEach { it.web.shouldBeNull() }
117+
}
118+
}
98119
}

0 commit comments

Comments
 (0)