Skip to content

m163 release #6885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Check Vertex AI Responses
name: Check Firebase AI Responses

on: pull_request

Expand All @@ -10,30 +10,30 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Clone mock responses
run: firebase-vertexai/update_responses.sh
run: firebase-ai/update_responses.sh
- name: Find cloned and latest versions
run: |
CLONED=$(git describe --tags)
LATEST=$(git tag --sort=v:refname | tail -n1)
echo "cloned_tag=$CLONED" >> $GITHUB_ENV
echo "latest_tag=$LATEST" >> $GITHUB_ENV
working-directory: firebase-vertexai/src/test/resources/vertexai-sdk-test-data
working-directory: firebase-ai/src/test/resources/vertexai-sdk-test-data
- name: Find comment from previous run if exists
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: fc
with:
issue-number: ${{github.event.number}}
body-includes: Vertex AI Mock Responses Check
body-includes: Firebase AI Mock Responses Check
- name: Comment on PR if newer version is available
if: ${{env.cloned_tag != env.latest_tag && !steps.fc.outputs.comment-id}}
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
issue-number: ${{github.event.number}}
body: >
### Vertex AI Mock Responses Check :warning:
### Firebase AI Mock Responses Check :warning:

A newer major version of the mock responses for Vertex AI unit tests is available.
[update_responses.sh](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-vertexai/update_responses.sh)
A newer major version of the mock responses for Firebase AI unit tests is available.
[update_responses.sh](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-ai/update_responses.sh)
should be updated to clone the latest version of the responses: `${{env.latest_tag}}`
- name: Delete comment when version gets updated
if: ${{env.cloned_tag == env.latest_tag && steps.fc.outputs.comment-id}}
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/dataconnect.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ jobs:
with:
node-version: ${{ env.FDC_NODEJS_VERSION }}

- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{ env.FDC_PYTHON_VERSION }}

- run: pip install -r firebase-dataconnect/ci/requirements.txt

- name: Install Firebase Tools ("firebase" command-line tool)
run: |
set -euo pipefail
Expand Down Expand Up @@ -229,6 +235,15 @@ jobs:
if: steps.connectedCheck.outcome != 'success'
run: |
set -euo pipefail

if [[ ! -e logcat.log ]] ; then
echo "WARNING dsdta43sxk: logcat log file not found; skipping scanning for test failures" >&2
else
echo "Scanning logcat output for failure details"
python firebase-dataconnect/ci/logcat_error_report.py --logcat-file=logcat.log
echo
fi

echo 'Failing because the outcome of the "Gradle connectedCheck" step ("${{ steps.connectedCheck.outcome }}") was not successful'
exit 1

Expand Down
1 change: 1 addition & 0 deletions firebase-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Unreleased
* [fixed] **Breaking Change**: Fixed missing builder methods and return types in builders.
* [changed] **Breaking Change**: `LiveModelFutures.connect` now returns `ListenableFuture<LiveSessionFutures>` instead of `ListenableFuture<LiveSession>`.
* **Action Required:** Remove any transformations from LiveSession object to LiveSessionFutures object.
* **Action Required:** Change type of variable handling `LiveModelFutures.connect` to `ListenableFuture<LiveSessionsFutures>`
Expand Down
21 changes: 15 additions & 6 deletions firebase-ai/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,10 @@ package com.google.firebase.ai.type {
method public <T extends com.google.firebase.ai.type.Part> com.google.firebase.ai.type.Content.Builder addPart(T data);
method public com.google.firebase.ai.type.Content.Builder addText(String text);
method public com.google.firebase.ai.type.Content build();
method public java.util.List<com.google.firebase.ai.type.Part> getParts();
method public String? getRole();
method public void setParts(java.util.List<com.google.firebase.ai.type.Part>);
method public void setRole(String?);
property public final java.util.List<com.google.firebase.ai.type.Part> parts;
property public final String? role;
method public com.google.firebase.ai.type.Content.Builder setParts(java.util.List<com.google.firebase.ai.type.Part> parts);
method public com.google.firebase.ai.type.Content.Builder setRole(String? role);
field public java.util.List<com.google.firebase.ai.type.Part> parts;
field public String? role;
}

public final class ContentBlockedException extends com.google.firebase.ai.type.FirebaseAIException {
Expand Down Expand Up @@ -355,6 +353,17 @@ package com.google.firebase.ai.type {
public static final class GenerationConfig.Builder {
ctor public GenerationConfig.Builder();
method public com.google.firebase.ai.type.GenerationConfig build();
method public com.google.firebase.ai.type.GenerationConfig.Builder setCandidateCount(Integer? candidateCount);
method public com.google.firebase.ai.type.GenerationConfig.Builder setFrequencyPenalty(Float? frequencyPenalty);
method public com.google.firebase.ai.type.GenerationConfig.Builder setMaxOutputTokens(Integer? maxOutputTokens);
method public com.google.firebase.ai.type.GenerationConfig.Builder setPresencePenalty(Float? presencePenalty);
method public com.google.firebase.ai.type.GenerationConfig.Builder setResponseMimeType(String? responseMimeType);
method public com.google.firebase.ai.type.GenerationConfig.Builder setResponseModalities(java.util.List<com.google.firebase.ai.type.ResponseModality>? responseModalities);
method public com.google.firebase.ai.type.GenerationConfig.Builder setResponseSchema(com.google.firebase.ai.type.Schema? responseSchema);
method public com.google.firebase.ai.type.GenerationConfig.Builder setStopSequences(java.util.List<java.lang.String>? stopSequences);
method public com.google.firebase.ai.type.GenerationConfig.Builder setTemperature(Float? temperature);
method public com.google.firebase.ai.type.GenerationConfig.Builder setTopK(Integer? topK);
method public com.google.firebase.ai.type.GenerationConfig.Builder setTopP(Float? topP);
field public Integer? candidateCount;
field public Float? frequencyPenalty;
field public Integer? maxOutputTokens;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ internal constructor(
"wss://firebasevertexai.googleapis.com/ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/$location?key=$key"

suspend fun getWebSocketSession(location: String): ClientWebSocketSession =
client.webSocketSession(getBidiEndpoint(location))
client.webSocketSession(getBidiEndpoint(location)) { applyCommonHeaders() }

fun generateContentStream(
request: GenerateContentRequest
): Flow<GenerateContentResponse.Internal> =
Expand All @@ -190,12 +191,7 @@ internal constructor(
throw FirebaseCommonAIException.from(e)
}

private fun HttpRequestBuilder.applyCommonConfiguration(request: Request) {
when (request) {
is GenerateContentRequest -> setBody<GenerateContentRequest>(request)
is CountTokensRequest -> setBody<CountTokensRequest>(request)
is GenerateImageRequest -> setBody<GenerateImageRequest>(request)
}
private fun HttpRequestBuilder.applyCommonHeaders() {
contentType(ContentType.Application.Json)
header("x-goog-api-key", key)
header("x-goog-api-client", apiClient)
Expand All @@ -204,6 +200,14 @@ internal constructor(
header("X-Firebase-AppVersion", appVersion)
}
}
private fun HttpRequestBuilder.applyCommonConfiguration(request: Request) {
when (request) {
is GenerateContentRequest -> setBody<GenerateContentRequest>(request)
is CountTokensRequest -> setBody<CountTokensRequest>(request)
is GenerateImageRequest -> setBody<GenerateImageRequest>(request)
}
applyCommonHeaders()
}

private suspend fun HttpRequestBuilder.applyHeaderProvider() {
if (headerProvider != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal constructor(
val groundingMetadata: GroundingMetadata? = null,
) {
internal fun toPublic(): Candidate {
val safetyRatings = safetyRatings?.map { it.toPublic() }.orEmpty()
val safetyRatings = safetyRatings?.mapNotNull { it.toPublic() }.orEmpty()
val citations = citationMetadata?.toPublic()
val finishReason = finishReason?.toPublic()

Expand Down Expand Up @@ -120,23 +120,31 @@ internal constructor(
internal data class Internal
@JvmOverloads
constructor(
val category: HarmCategory.Internal,
val probability: HarmProbability.Internal,
val category: HarmCategory.Internal? = null,
val probability: HarmProbability.Internal? = null,
val blocked: Boolean? = null, // TODO(): any reason not to default to false?
val probabilityScore: Float? = null,
val severity: HarmSeverity.Internal? = null,
val severityScore: Float? = null,
) {

internal fun toPublic() =
SafetyRating(
category = category.toPublic(),
probability = probability.toPublic(),
probabilityScore = probabilityScore ?: 0f,
blocked = blocked,
severity = severity?.toPublic(),
severityScore = severityScore
)
// Due to a bug in the backend, it's possible that we receive
// an invalid `SafetyRating` value, without either category or
// probability. We return null in those cases to enable
// filtering by the higher level types.
if (category == null || probability == null) {
null
} else {
SafetyRating(
category = category.toPublic(),
probability = probability.toPublic(),
probabilityScore = probabilityScore ?: 0f,
blocked = blocked,
severity = severity?.toPublic(),
severityScore = severityScore
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@ constructor(public val role: String? = "user", public val parts: List<Part>) {
public class Builder {

/** The producer of the content. Must be either 'user' or 'model'. By default, it's "user". */
public var role: String? = "user"
@JvmField public var role: String? = "user"

/**
* The mutable list of [Part]s comprising the [Content].
*
* Prefer using the provided helper methods over modifying this list directly.
*/
public var parts: MutableList<Part> = arrayListOf()
@JvmField public var parts: MutableList<Part> = arrayListOf()

public fun setRole(role: String?): Content.Builder = apply { this.role = role }
public fun setParts(parts: MutableList<Part>): Content.Builder = apply { this.parts = parts }

/** Adds a new [Part] to [parts]. */
@JvmName("addPart")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,36 @@ private constructor(
@JvmField public var responseSchema: Schema? = null
@JvmField public var responseModalities: List<ResponseModality>? = null

public fun setTemperature(temperature: Float?): Builder = apply {
this.temperature = temperature
}
public fun setTopK(topK: Int?): Builder = apply { this.topK = topK }
public fun setTopP(topP: Float?): Builder = apply { this.topP = topP }
public fun setCandidateCount(candidateCount: Int?): Builder = apply {
this.candidateCount = candidateCount
}
public fun setMaxOutputTokens(maxOutputTokens: Int?): Builder = apply {
this.maxOutputTokens = maxOutputTokens
}
public fun setPresencePenalty(presencePenalty: Float?): Builder = apply {
this.presencePenalty = presencePenalty
}
public fun setFrequencyPenalty(frequencyPenalty: Float?): Builder = apply {
this.frequencyPenalty = frequencyPenalty
}
public fun setStopSequences(stopSequences: List<String>?): Builder = apply {
this.stopSequences = stopSequences
}
public fun setResponseMimeType(responseMimeType: String?): Builder = apply {
this.responseMimeType = responseMimeType
}
public fun setResponseSchema(responseSchema: Schema?): Builder = apply {
this.responseSchema = responseSchema
}
public fun setResponseModalities(responseModalities: List<ResponseModality>?): Builder = apply {
this.responseModalities = responseModalities
}

/** Create a new [GenerationConfig] with the attached arguments. */
public fun build(): GenerationConfig =
GenerationConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class PromptFeedback(
) {

internal fun toPublic(): PromptFeedback {
val safetyRatings = safetyRatings?.map { it.toPublic() }.orEmpty()
val safetyRatings = safetyRatings?.mapNotNull { it.toPublic() }.orEmpty()
return PromptFeedback(blockReason?.toPublic(), safetyRatings, blockReasonMessage)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ internal class DevAPIStreamingSnapshotTests {
withTimeout(testTimeout) {
val responseList = responses.toList()
responseList.isEmpty() shouldBe false
responseList.first().candidates.first().finishReason shouldBe FinishReason.STOP
responseList.first().candidates.first().content.parts.isEmpty() shouldBe false
responseList.first().candidates.first().safetyRatings.isEmpty() shouldBe false
responseList.last().candidates.first().apply {
finishReason shouldBe FinishReason.STOP
content.parts.isEmpty() shouldBe false
}
}
}

Expand All @@ -56,10 +57,9 @@ internal class DevAPIStreamingSnapshotTests {
withTimeout(testTimeout) {
val responseList = responses.toList()
responseList.isEmpty() shouldBe false
responseList.forEach {
it.candidates.first().finishReason shouldBe FinishReason.STOP
it.candidates.first().content.parts.isEmpty() shouldBe false
it.candidates.first().safetyRatings.isEmpty() shouldBe false
responseList.last().candidates.first().apply {
finishReason shouldBe FinishReason.STOP
content.parts.isEmpty() shouldBe false
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.withTimeout
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
internal class VertexAIStreamingSnapshotTests {
private val testTimeout = 5.seconds

Expand All @@ -48,9 +51,11 @@ internal class VertexAIStreamingSnapshotTests {
withTimeout(testTimeout) {
val responseList = responses.toList()
responseList.isEmpty() shouldBe false
responseList.first().candidates.first().finishReason shouldBe FinishReason.STOP
responseList.first().candidates.first().content.parts.isEmpty() shouldBe false
responseList.first().candidates.first().safetyRatings.isEmpty() shouldBe false
responseList.last().candidates.first().apply {
finishReason shouldBe FinishReason.STOP
content.parts.isEmpty() shouldBe false
safetyRatings.isEmpty() shouldBe false
}
}
}

Expand All @@ -62,10 +67,9 @@ internal class VertexAIStreamingSnapshotTests {
withTimeout(testTimeout) {
val responseList = responses.toList()
responseList.isEmpty() shouldBe false
responseList.forEach {
it.candidates.first().finishReason shouldBe FinishReason.STOP
it.candidates.first().content.parts.isEmpty() shouldBe false
it.candidates.first().safetyRatings.isEmpty() shouldBe false
responseList.last().candidates.first().apply {
finishReason shouldBe FinishReason.STOP
content.parts.isEmpty() shouldBe false
}
}
}
Expand All @@ -85,6 +89,18 @@ internal class VertexAIStreamingSnapshotTests {
}
}

@Test
fun `invalid safety ratings during image generation`() =
goldenVertexStreamingFile("streaming-success-image-invalid-safety-ratings.txt") {
val responses = model.generateContentStream("prompt")

withTimeout(testTimeout) {
val responseList = responses.toList()

responseList.isEmpty() shouldBe false
}
}

@Test
fun `unknown enum in finish reason`() =
goldenVertexStreamingFile("streaming-failure-unknown-finish-enum.txt") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.json.JSONArray
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@OptIn(PublicPreviewAPI::class)
@RunWith(RobolectricTestRunner::class)
internal class VertexAIUnarySnapshotTests {
private val testTimeout = 5.seconds

Expand Down Expand Up @@ -125,6 +128,16 @@ internal class VertexAIUnarySnapshotTests {
}
}

@Test
fun `invalid safety ratings during image generation`() =
goldenVertexUnaryFile("unary-success-image-invalid-safety-ratings.json") {
withTimeout(testTimeout) {
val response = model.generateContent("prompt")

response.candidates.isEmpty() shouldBe false
}
}

@Test
fun `unknown enum in finish reason`() =
goldenVertexUnaryFile("unary-failure-unknown-enum-finish-reason.json") {
Expand Down
Loading
Loading