Skip to content

Commit 04ba803

Browse files
committed
[AI] Correctly handle empty candidates in the accessors
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. Rename file
1 parent 628a6c6 commit 04ba803

File tree

2 files changed

+36
-14
lines changed

2 files changed

+36
-14
lines changed

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ public class GenerateContentResponse(
3636
* exists.
3737
*/
3838
public val text: String? by lazy {
39-
candidates.first().content.parts.filterIsInstance<TextPart>().joinToString(" ") { it.text }
39+
candidates.firstOrNull()?.content?.parts?.filterIsInstance<TextPart>()?.joinToString(" ") {
40+
it.text
41+
}
4042
}
4143

4244
/** Convenience field to list all the [FunctionCallPart]s in the response, if they exist. */
4345
public val functionCalls: List<FunctionCallPart> by lazy {
44-
candidates.first().content.parts.filterIsInstance<FunctionCallPart>()
46+
candidates.firstOrNull()?.content?.parts?.filterIsInstance<FunctionCallPart>().orEmpty()
4547
}
4648

4749
/**
@@ -50,10 +52,15 @@ public class GenerateContentResponse(
5052
* This also includes any [ImagePart], but they will be represented as [InlineDataPart] instead.
5153
*/
5254
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-
}
55+
candidates
56+
.firstOrNull()
57+
?.content
58+
?.parts
59+
?.let { parts ->
60+
parts.filterIsInstance<ImagePart>().map { it.toInlineDataPart() } +
61+
parts.filterIsInstance<InlineDataPart>()
62+
}
63+
.orEmpty()
5764
}
5865

5966
@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)