diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index 988d8d96b4a..eda86c88799 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -1,7 +1,8 @@ # Unreleased - [changed] **Breaking Change**: Removed the `candidateCount` option from `LiveGenerationConfig` -- [changed] Added support for the URL context tool, which allows the model to access content from provided public web URLs to inform and enhance its responses. +- [changed] Added support for the URL context tool, which allows the model to access content from + provided public web URLs to inform and enhance its responses. # 17.3.0 diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt index 47038321793..bcadf6f292f 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt @@ -38,15 +38,17 @@ import kotlinx.serialization.json.JsonNames * enabled. */ public class Candidate +@OptIn(PublicPreviewAPI::class) internal constructor( public val content: Content, public val safetyRatings: List, public val citationMetadata: CitationMetadata?, public val finishReason: FinishReason?, public val groundingMetadata: GroundingMetadata?, - public val urlContextMetadata: UrlContextMetadata? + @property:PublicPreviewAPI public val urlContextMetadata: UrlContextMetadata? ) { + @OptIn(PublicPreviewAPI::class) @Serializable internal data class Internal( val content: Content.Internal? = null, @@ -56,6 +58,8 @@ internal constructor( val groundingMetadata: GroundingMetadata.Internal? = null, val urlContextMetadata: UrlContextMetadata.Internal? = null ) { + + @OptIn(PublicPreviewAPI::class) internal fun toPublic(): Candidate { val safetyRatings = safetyRatings?.mapNotNull { it.toPublic() }.orEmpty() val citations = citationMetadata?.toPublic() @@ -505,7 +509,9 @@ public class Segment( */ @PublicPreviewAPI public class UrlContextMetadata internal constructor(public val urlMetadata: List) { + @Serializable + @PublicPreviewAPI internal data class Internal(val urlMetadata: List?) { internal fun toPublic() = UrlContextMetadata(urlMetadata?.map { it.toPublic() } ?: emptyList()) } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt index bc4a53cc8eb..6ee2ea737dd 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt @@ -23,4 +23,9 @@ package com.google.firebase.ai.type "This API is part of an experimental public preview and may change in " + "backwards-incompatible ways without notice.", ) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY +) public annotation class PublicPreviewAPI() diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt index 7dbf19cb48c..6c307cea0fe 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt @@ -24,12 +24,15 @@ import kotlinx.serialization.json.JsonObject * can be used to gather information or complete tasks. */ public class Tool +@OptIn(PublicPreviewAPI::class) internal constructor( internal val functionDeclarations: List?, internal val googleSearch: GoogleSearch?, internal val codeExecution: JsonObject?, - internal val urlContext: UrlContext?, + @property:PublicPreviewAPI internal val urlContext: UrlContext?, ) { + + @OptIn(PublicPreviewAPI::class) internal fun toInternal() = Internal( functionDeclarations?.map { it.toInternal() } ?: emptyList(), @@ -37,6 +40,8 @@ internal constructor( codeExecution = this.codeExecution, urlContext = this.urlContext?.toInternal() ) + + @OptIn(PublicPreviewAPI::class) @Serializable internal data class Internal( val functionDeclarations: List? = null, @@ -47,6 +52,7 @@ internal constructor( ) public companion object { + @OptIn(PublicPreviewAPI::class) private val codeExecutionInstance by lazy { Tool(null, null, JsonObject(emptyMap()), null) } /** @@ -56,6 +62,7 @@ internal constructor( */ @JvmStatic public fun functionDeclarations(functionDeclarations: List): Tool { + @OptIn(PublicPreviewAPI::class) return Tool(functionDeclarations, null, null, null) } @@ -97,6 +104,7 @@ internal constructor( */ @JvmStatic public fun googleSearch(googleSearch: GoogleSearch = GoogleSearch()): Tool { + @OptIn(PublicPreviewAPI::class) return Tool(null, googleSearch, null, null) } } diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt index 68e0b029ccc..e0ba386091b 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt @@ -18,6 +18,7 @@ package com.google.firebase.ai import com.google.firebase.ai.type.FinishReason import com.google.firebase.ai.type.InvalidAPIKeyException +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.ResponseStoppedException import com.google.firebase.ai.type.ServerException import com.google.firebase.ai.type.UrlRetrievalStatus @@ -135,6 +136,7 @@ internal class DevAPIUnarySnapshotTests { } } + @OptIn(PublicPreviewAPI::class) @Test fun `url context`() = goldenDevAPIUnaryFile("unary-success-url-context.json") { @@ -167,6 +169,7 @@ internal class DevAPIUnarySnapshotTests { } } + @OptIn(PublicPreviewAPI::class) @Test fun `url context mixed validity`() = goldenDevAPIUnaryFile("unary-success-url-context-mixed-validity.json") { diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt index 8301f48d968..1c6e087dd80 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt @@ -23,6 +23,7 @@ import com.google.firebase.ai.common.util.doBlocking import com.google.firebase.ai.type.Candidate import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.ServerException import com.google.firebase.ai.type.TextPart @@ -41,7 +42,6 @@ import io.ktor.http.content.TextContent import io.ktor.http.headersOf import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.withTimeout -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString import org.junit.Before import org.junit.Test @@ -146,7 +146,7 @@ internal class GenerativeModelTesting { exception.message shouldContain "location" } - @OptIn(ExperimentalSerializationApi::class) + @OptIn(PublicPreviewAPI::class) private fun generateContentResponseAsJsonString(text: String): String { return JSON.encodeToString( GenerateContentResponse.Internal( diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt index aa47afae7da..8804e386d33 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt @@ -26,6 +26,7 @@ import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.CountTokensResponse import com.google.firebase.ai.type.FunctionCallingConfig import com.google.firebase.ai.type.GoogleSearch +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.TextPart import com.google.firebase.ai.type.Tool @@ -285,6 +286,7 @@ internal class RequestFormatTests { ) withTimeout(5.seconds) { + @OptIn(PublicPreviewAPI::class) controller .generateContentStream( GenerateContentRequest( @@ -323,6 +325,7 @@ internal class RequestFormatTests { ) withTimeout(5.seconds) { + @OptIn(PublicPreviewAPI::class) controller .generateContentStream( GenerateContentRequest( @@ -432,6 +435,7 @@ internal class RequestFormatTests { ) withTimeout(5.seconds) { + @OptIn(PublicPreviewAPI::class) controller .generateContentStream( GenerateContentRequest( diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt index 6cc501cedd5..d9081cae371 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt @@ -24,6 +24,7 @@ import com.google.firebase.ai.common.JSON import com.google.firebase.ai.type.Candidate import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.TextPart import io.ktor.client.engine.mock.MockEngine @@ -32,7 +33,6 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.headersOf import io.ktor.utils.io.ByteChannel -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString import org.mockito.Mockito @@ -44,7 +44,7 @@ internal fun prepareStreamingResponse( response: List ): List = response.map { "data: ${JSON.encodeToString(it)}$SSE_SEPARATOR".toByteArray() } -@OptIn(ExperimentalSerializationApi::class) +@OptIn(PublicPreviewAPI::class) internal fun createResponses(vararg text: String): List { val candidates = text.map { Candidate.Internal(Content.Internal(parts = listOf(TextPart.Internal(it)))) } diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/ToolTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/ToolTest.kt index bb967470d84..538792be5f7 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/type/ToolTest.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/ToolTest.kt @@ -28,6 +28,7 @@ internal class ToolTest { tool.googleSearch.shouldNotBeNull() tool.functionDeclarations.shouldBeNull() tool.codeExecution.shouldBeNull() + @OptIn(PublicPreviewAPI::class) tool.urlContext.shouldBeNull() } @@ -39,6 +40,7 @@ internal class ToolTest { tool.functionDeclarations?.first() shouldBe functionDeclaration tool.googleSearch.shouldBeNull() tool.codeExecution.shouldBeNull() + @OptIn(PublicPreviewAPI::class) tool.urlContext.shouldBeNull() } @@ -48,9 +50,11 @@ internal class ToolTest { tool.codeExecution.shouldNotBeNull() tool.functionDeclarations.shouldBeNull() tool.googleSearch.shouldBeNull() + @OptIn(PublicPreviewAPI::class) tool.urlContext.shouldBeNull() } + @OptIn(PublicPreviewAPI::class) @Test fun `urlContext() creates a tool with a urlContext property`() { val tool = Tool.urlContext() @@ -58,6 +62,7 @@ internal class ToolTest { tool.googleSearch.shouldBeNull() tool.functionDeclarations.shouldBeNull() tool.codeExecution.shouldBeNull() + @OptIn(PublicPreviewAPI::class) tool.urlContext.shouldNotBeNull() } }