diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index f566974f52e..00c3cf8c842 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -1,4 +1,6 @@ # Unreleased +* [fixed] Fixed an issue causing the accessor methods in `GenerateContentResponse` to throw an exception + when the response contained no candidates. * [changed] Added better description for requests which fail due to the Gemini API not being configured. * [changed] Added a `dilation` parameter to `ImagenMaskReference.generateMaskAndPadForOutpainting` @@ -6,7 +8,7 @@ # 17.1.0 ======= -* [feature] added support for Imagen Editing, including inpainting, outpainting, control, style +* [feature] added support for Imagen Editing, including inpainting, outpainting, control, style transfer, and subject references (#7075) * [feature] **Preview:** Added support for bidirectional streaming in Gemini Developer Api @@ -51,4 +53,3 @@ Note: This feature is in Public Preview, which means that it is not subject to any SLA or deprecation policy and could change in backwards-incompatible ways. - diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt index be2b50f3be4..7e1b44106a2 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt @@ -32,28 +32,42 @@ public class GenerateContentResponse( public val usageMetadata: UsageMetadata?, ) { /** - * Convenience field representing all the text parts in the response as a single string, if they - * exists. + * Convenience field representing all the text parts in the response as a single string. + * + * The value is null if the response contains no [candidates]. */ public val text: String? by lazy { - candidates.first().content.parts.filterIsInstance().joinToString(" ") { it.text } + candidates.firstOrNull()?.content?.parts?.filterIsInstance()?.joinToString(" ") { + it.text + } } - /** Convenience field to list all the [FunctionCallPart]s in the response, if they exist. */ + /** + * Convenience field to list all the [FunctionCallPart]s in the response. + * + * The value is an empty list if the response contains no [candidates]. + */ public val functionCalls: List by lazy { - candidates.first().content.parts.filterIsInstance() + candidates.firstOrNull()?.content?.parts?.filterIsInstance().orEmpty() } /** * Convenience field representing all the [InlineDataPart]s in the first candidate, if they exist. * * This also includes any [ImagePart], but they will be represented as [InlineDataPart] instead. + * + * The value is an empty list if the response contains no [candidates]. */ public val inlineDataParts: List by lazy { - candidates.first().content.parts.let { parts -> - parts.filterIsInstance().map { it.toInlineDataPart() } + - parts.filterIsInstance() - } + candidates + .firstOrNull() + ?.content + ?.parts + ?.let { parts -> + parts.filterIsInstance().map { it.toInlineDataPart() } + + parts.filterIsInstance() + } + .orEmpty() } @Serializable 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 85c99210a04..bd2f69f79f4 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 @@ -22,12 +22,12 @@ import com.google.firebase.ai.type.ResponseStoppedException import com.google.firebase.ai.type.ServerException import com.google.firebase.ai.util.goldenDevAPIUnaryFile import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldNotBeEmpty import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.should import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe import io.ktor.http.HttpStatusCode import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.withTimeout @@ -42,9 +42,24 @@ internal class DevAPIUnarySnapshotTests { withTimeout(testTimeout) { val response = model.generateContent("prompt") - response.candidates.isEmpty() shouldBe false + response.candidates.shouldNotBeEmpty() response.candidates.first().finishReason shouldBe FinishReason.STOP - response.candidates.first().content.parts.isEmpty() shouldBe false + response.candidates.first().content.parts.shouldNotBeEmpty() + } + } + + @Test + fun `only prompt feedback reply`() = + goldenDevAPIUnaryFile("unary-failure-only-prompt-feedback.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldBeEmpty() + + // Check response from accessors + response.text.shouldBeNull() + response.functionCalls.shouldBeEmpty() + response.inlineDataParts.shouldBeEmpty() } } @@ -54,9 +69,9 @@ internal class DevAPIUnarySnapshotTests { withTimeout(testTimeout) { val response = model.generateContent("prompt") - response.candidates.isEmpty() shouldBe false + response.candidates.shouldNotBeEmpty() response.candidates.first().finishReason shouldBe FinishReason.STOP - response.candidates.first().content.parts.isEmpty() shouldBe false + response.candidates.first().content.parts.shouldNotBeEmpty() } } @@ -66,11 +81,11 @@ internal class DevAPIUnarySnapshotTests { withTimeout(testTimeout) { val response = model.generateContent("prompt") - response.candidates.isEmpty() shouldBe false + response.candidates.shouldNotBeEmpty() response.candidates.first().citationMetadata?.citations?.size shouldBe 4 response.candidates.first().citationMetadata?.citations?.forEach { - it.startIndex shouldNotBe null - it.endIndex shouldNotBe null + it.startIndex.shouldNotBeNull() + it.endIndex.shouldNotBeNull() } } }