Skip to content

Commit c85227e

Browse files
authored
[AI] Correctly handle empty candidates in the accessors (#7270)
Before, if a response had no candidates, accessors would throw an exception when used instead of handle the case elegantly. Now they either return an empty list, for collections, or null, for string. The test file is added in FirebaseExtended/vertexai-sdk-test-data#48
1 parent da659d7 commit c85227e

File tree

3 files changed

+49
-19
lines changed

3 files changed

+49
-19
lines changed

firebase-ai/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Unreleased
2+
* [fixed] Fixed an issue causing the accessor methods in `GenerateContentResponse` to throw an exception
3+
when the response contained no candidates.
24
* [changed] Added better description for requests which fail due to the Gemini API not being
35
configured.
46
* [changed] Added a `dilation` parameter to `ImagenMaskReference.generateMaskAndPadForOutpainting`
57
(#7260)
68

79
# 17.1.0
810
=======
9-
* [feature] added support for Imagen Editing, including inpainting, outpainting, control, style
11+
* [feature] added support for Imagen Editing, including inpainting, outpainting, control, style
1012
transfer, and subject references (#7075)
1113
* [feature] **Preview:** Added support for bidirectional streaming in Gemini Developer Api
1214

@@ -51,4 +53,3 @@
5153

5254
Note: This feature is in Public Preview, which means that it is not subject to any SLA or
5355
deprecation policy and could change in backwards-incompatible ways.
54-

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,42 @@ public class GenerateContentResponse(
3232
public val usageMetadata: UsageMetadata?,
3333
) {
3434
/**
35-
* Convenience field representing all the text parts in the response as a single string, if they
36-
* exists.
35+
* Convenience field representing all the text parts in the response as a single string.
36+
*
37+
* The value is null if the response contains no [candidates].
3738
*/
3839
public val text: String? by lazy {
39-
candidates.first().content.parts.filterIsInstance<TextPart>().joinToString(" ") { it.text }
40+
candidates.firstOrNull()?.content?.parts?.filterIsInstance<TextPart>()?.joinToString(" ") {
41+
it.text
42+
}
4043
}
4144

42-
/** Convenience field to list all the [FunctionCallPart]s in the response, if they exist. */
45+
/**
46+
* Convenience field to list all the [FunctionCallPart]s in the response.
47+
*
48+
* The value is an empty list if the response contains no [candidates].
49+
*/
4350
public val functionCalls: List<FunctionCallPart> by lazy {
44-
candidates.first().content.parts.filterIsInstance<FunctionCallPart>()
51+
candidates.firstOrNull()?.content?.parts?.filterIsInstance<FunctionCallPart>().orEmpty()
4552
}
4653

4754
/**
4855
* Convenience field representing all the [InlineDataPart]s in the first candidate, if they exist.
4956
*
5057
* This also includes any [ImagePart], but they will be represented as [InlineDataPart] instead.
58+
*
59+
* The value is an empty list if the response contains no [candidates].
5160
*/
5261
public val inlineDataParts: List<InlineDataPart> by lazy {
53-
candidates.first().content.parts.let { parts ->
54-
parts.filterIsInstance<ImagePart>().map { it.toInlineDataPart() } +
55-
parts.filterIsInstance<InlineDataPart>()
56-
}
62+
candidates
63+
.firstOrNull()
64+
?.content
65+
?.parts
66+
?.let { parts ->
67+
parts.filterIsInstance<ImagePart>().map { it.toInlineDataPart() } +
68+
parts.filterIsInstance<InlineDataPart>()
69+
}
70+
.orEmpty()
5771
}
5872

5973
@Serializable

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ 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.shouldBeEmpty
2526
import io.kotest.matchers.collections.shouldNotBeEmpty
2627
import io.kotest.matchers.nulls.shouldBeNull
2728
import io.kotest.matchers.nulls.shouldNotBeNull
2829
import io.kotest.matchers.should
2930
import io.kotest.matchers.shouldBe
30-
import io.kotest.matchers.shouldNotBe
3131
import io.ktor.http.HttpStatusCode
3232
import kotlin.time.Duration.Companion.seconds
3333
import kotlinx.coroutines.withTimeout
@@ -42,9 +42,24 @@ internal class DevAPIUnarySnapshotTests {
4242
withTimeout(testTimeout) {
4343
val response = model.generateContent("prompt")
4444

45-
response.candidates.isEmpty() shouldBe false
45+
response.candidates.shouldNotBeEmpty()
4646
response.candidates.first().finishReason shouldBe FinishReason.STOP
47-
response.candidates.first().content.parts.isEmpty() shouldBe false
47+
response.candidates.first().content.parts.shouldNotBeEmpty()
48+
}
49+
}
50+
51+
@Test
52+
fun `only prompt feedback reply`() =
53+
goldenDevAPIUnaryFile("unary-failure-only-prompt-feedback.json") {
54+
withTimeout(testTimeout) {
55+
val response = model.generateContent("prompt")
56+
57+
response.candidates.shouldBeEmpty()
58+
59+
// Check response from accessors
60+
response.text.shouldBeNull()
61+
response.functionCalls.shouldBeEmpty()
62+
response.inlineDataParts.shouldBeEmpty()
4863
}
4964
}
5065

@@ -54,9 +69,9 @@ internal class DevAPIUnarySnapshotTests {
5469
withTimeout(testTimeout) {
5570
val response = model.generateContent("prompt")
5671

57-
response.candidates.isEmpty() shouldBe false
72+
response.candidates.shouldNotBeEmpty()
5873
response.candidates.first().finishReason shouldBe FinishReason.STOP
59-
response.candidates.first().content.parts.isEmpty() shouldBe false
74+
response.candidates.first().content.parts.shouldNotBeEmpty()
6075
}
6176
}
6277

@@ -66,11 +81,11 @@ internal class DevAPIUnarySnapshotTests {
6681
withTimeout(testTimeout) {
6782
val response = model.generateContent("prompt")
6883

69-
response.candidates.isEmpty() shouldBe false
84+
response.candidates.shouldNotBeEmpty()
7085
response.candidates.first().citationMetadata?.citations?.size shouldBe 4
7186
response.candidates.first().citationMetadata?.citations?.forEach {
72-
it.startIndex shouldNotBe null
73-
it.endIndex shouldNotBe null
87+
it.startIndex.shouldNotBeNull()
88+
it.endIndex.shouldNotBeNull()
7489
}
7590
}
7691
}

0 commit comments

Comments
 (0)