Skip to content

Commit eea0fc1

Browse files
authored
Merge branch 'main' into ep/unit-test-report-action
2 parents fe91d08 + a0911a7 commit eea0fc1

File tree

46 files changed

+951
-96
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+951
-96
lines changed

firebase-ai/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Unreleased
22

3+
- [fixed] Fixed an issue causing streaming chat interactions to drop thought signatures. (#7562)
4+
5+
# 17.6.0
6+
7+
- [feature] Added support for server templates via `TemplateGenerativeModel` and
8+
`TemplateImagenModel`. (#7503)
9+
10+
# 17.5.0
11+
312
- [changed] Added better scheduling and louder output for Live API.
413
- [changed] Added support for input and output transcription. (#7482)
514
- [feature] Added support for sending realtime audio and video in a `LiveSession`.

firebase-ai/api.txt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ package com.google.firebase.ai {
3636
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List<com.google.firebase.ai.type.Tool>? tools = null);
3737
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List<com.google.firebase.ai.type.Tool>? tools = null, com.google.firebase.ai.type.Content? systemInstruction = null);
3838
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List<com.google.firebase.ai.type.Tool>? tools = null, com.google.firebase.ai.type.Content? systemInstruction = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions());
39+
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.TemplateGenerativeModel templateGenerativeModel();
40+
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.TemplateGenerativeModel templateGenerativeModel(com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions());
41+
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.TemplateImagenModel templateImagenModel();
42+
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.TemplateImagenModel templateImagenModel(com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions());
3943
property public static final com.google.firebase.ai.FirebaseAI instance;
4044
field public static final com.google.firebase.ai.FirebaseAI.Companion Companion;
4145
}
@@ -83,6 +87,15 @@ package com.google.firebase.ai {
8387
method public suspend Object? connect(kotlin.coroutines.Continuation<? super com.google.firebase.ai.type.LiveSession>);
8488
}
8589

90+
@com.google.firebase.ai.type.PublicPreviewAPI public final class TemplateGenerativeModel {
91+
method public suspend Object? generateContent(String templateId, java.util.Map<java.lang.String,?> inputs, kotlin.coroutines.Continuation<? super com.google.firebase.ai.type.GenerateContentResponse>);
92+
method public kotlinx.coroutines.flow.Flow<com.google.firebase.ai.type.GenerateContentResponse> generateContentStream(String templateId, java.util.Map<java.lang.String,?> inputs);
93+
}
94+
95+
@com.google.firebase.ai.type.PublicPreviewAPI public final class TemplateImagenModel {
96+
method public suspend Object? generateImages(String templateId, java.util.Map<java.lang.String,?> inputs, kotlin.coroutines.Continuation<? super com.google.firebase.ai.type.ImagenGenerationResponse<com.google.firebase.ai.type.ImagenInlineImage>>);
97+
}
98+
8699
}
87100

88101
package com.google.firebase.ai.java {
@@ -166,6 +179,29 @@ package com.google.firebase.ai.java {
166179
method public com.google.firebase.ai.java.LiveSessionFutures from(com.google.firebase.ai.type.LiveSession session);
167180
}
168181

182+
public abstract class TemplateGenerativeModelFutures {
183+
method public static final com.google.firebase.ai.java.TemplateGenerativeModelFutures from(com.google.firebase.ai.TemplateGenerativeModel model);
184+
method public abstract com.google.common.util.concurrent.ListenableFuture<com.google.firebase.ai.type.GenerateContentResponse> generateContent(String templateId, java.util.Map<java.lang.String,?> inputs);
185+
method public abstract org.reactivestreams.Publisher<com.google.firebase.ai.type.GenerateContentResponse> generateContentStream(String templateId, java.util.Map<java.lang.String,?> inputs);
186+
method public abstract com.google.firebase.ai.TemplateGenerativeModel getGenerativeModel();
187+
field public static final com.google.firebase.ai.java.TemplateGenerativeModelFutures.Companion Companion;
188+
}
189+
190+
public static final class TemplateGenerativeModelFutures.Companion {
191+
method public com.google.firebase.ai.java.TemplateGenerativeModelFutures from(com.google.firebase.ai.TemplateGenerativeModel model);
192+
}
193+
194+
public abstract class TemplateImagenModelFutures {
195+
method public static final com.google.firebase.ai.java.TemplateImagenModelFutures from(com.google.firebase.ai.TemplateImagenModel model);
196+
method public abstract com.google.common.util.concurrent.ListenableFuture<com.google.firebase.ai.type.ImagenGenerationResponse<com.google.firebase.ai.type.ImagenInlineImage>> generateImages(String templateId, java.util.Map<java.lang.String,?> inputs);
197+
method public abstract com.google.firebase.ai.TemplateImagenModel getImageModel();
198+
field public static final com.google.firebase.ai.java.TemplateImagenModelFutures.Companion Companion;
199+
}
200+
201+
public static final class TemplateImagenModelFutures.Companion {
202+
method public com.google.firebase.ai.java.TemplateImagenModelFutures from(com.google.firebase.ai.TemplateImagenModel model);
203+
}
204+
169205
}
170206

171207
package com.google.firebase.ai.type {

firebase-ai/gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
version=17.5.0
16-
latestReleasedVersion=17.4.0
15+
version=17.6.1
16+
latestReleasedVersion=17.6.0

firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.google.firebase.ai.type.GenerateContentResponse
2222
import com.google.firebase.ai.type.ImagePart
2323
import com.google.firebase.ai.type.InlineDataPart
2424
import com.google.firebase.ai.type.InvalidStateException
25+
import com.google.firebase.ai.type.Part
2526
import com.google.firebase.ai.type.TextPart
2627
import com.google.firebase.ai.type.content
2728
import java.util.LinkedList
@@ -133,6 +134,7 @@ public class Chat(
133134
val bitmaps = LinkedList<Bitmap>()
134135
val inlineDataParts = LinkedList<InlineDataPart>()
135136
val text = StringBuilder()
137+
val parts = mutableListOf<Part>()
136138

137139
/**
138140
* TODO: revisit when images and inline data are returned. This will cause issues with how
@@ -147,22 +149,17 @@ public class Chat(
147149
is ImagePart -> bitmaps.add(part.image)
148150
is InlineDataPart -> inlineDataParts.add(part)
149151
}
152+
parts.add(part)
150153
}
151154
}
152155
.onCompletion {
153156
lock.release()
154157
if (it == null) {
155158
val content =
156159
content("model") {
157-
for (bitmap in bitmaps) {
158-
image(bitmap)
159-
}
160-
for (inlineDataPart in inlineDataParts) {
161-
inlineData(inlineDataPart.inlineData, inlineDataPart.mimeType)
162-
}
163-
if (text.isNotBlank()) {
164-
text(text.toString())
165-
}
160+
setParts(
161+
parts.filterNot { part -> part is TextPart && !part.hasContent() }.toMutableList()
162+
)
166163
}
167164

168165
history.add(prompt)
@@ -224,3 +221,12 @@ public class Chat(
224221
}
225222
}
226223
}
224+
225+
/**
226+
* Returns true if the [TextPart] contains any content, either in its [TextPart.text] property or
227+
* its [TextPart.thoughtSignature] property.
228+
*/
229+
private fun TextPart.hasContent(): Boolean {
230+
if (text.isNotEmpty()) return true
231+
return !thoughtSignature.isNullOrBlank()
232+
}

firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,29 @@ internal constructor(
106106
)
107107
}
108108

109+
/**
110+
* Instantiates a new [TemplateGenerativeModel] given the provided parameters.
111+
*
112+
* @param requestOptions Configuration options for sending requests to the backend.
113+
* @return The initialized [TemplateGenerativeModel] instance.
114+
*/
115+
@JvmOverloads
116+
@PublicPreviewAPI
117+
public fun templateGenerativeModel(
118+
requestOptions: RequestOptions = RequestOptions(),
119+
): TemplateGenerativeModel {
120+
val templateUri = getTemplateUri(backend)
121+
return TemplateGenerativeModel(
122+
templateUri,
123+
firebaseApp.options.apiKey,
124+
firebaseApp,
125+
useLimitedUseAppCheckTokens,
126+
requestOptions,
127+
appCheckProvider.get(),
128+
internalAuthProvider.get(),
129+
)
130+
}
131+
109132
/**
110133
* Instantiates a new [LiveGenerationConfig] given the provided parameters.
111134
*
@@ -205,6 +228,29 @@ internal constructor(
205228
)
206229
}
207230

231+
/**
232+
* Instantiates a new [TemplateImagenModel] given the provided parameters.
233+
*
234+
* @param requestOptions Configuration options for sending requests to the backend.
235+
* @return The initialized [TemplateImagenModel] instance.
236+
*/
237+
@JvmOverloads
238+
@PublicPreviewAPI
239+
public fun templateImagenModel(
240+
requestOptions: RequestOptions = RequestOptions(),
241+
): TemplateImagenModel {
242+
val templateUri = getTemplateUri(backend)
243+
return TemplateImagenModel(
244+
templateUri,
245+
firebaseApp.options.apiKey,
246+
firebaseApp,
247+
useLimitedUseAppCheckTokens,
248+
requestOptions,
249+
appCheckProvider.get(),
250+
internalAuthProvider.get(),
251+
)
252+
}
253+
208254
public companion object {
209255
/** The [FirebaseAI] instance for the default [FirebaseApp] using the Google AI Backend. */
210256
@JvmStatic
@@ -258,6 +304,13 @@ internal constructor(
258304

259305
private val TAG = FirebaseAI::class.java.simpleName
260306
}
307+
308+
private fun getTemplateUri(backend: GenerativeBackend): String =
309+
when (backend.backend) {
310+
GenerativeBackendEnum.VERTEX_AI ->
311+
"projects/${firebaseApp.options.projectId}/locations/${backend.location}/templates/"
312+
GenerativeBackendEnum.GOOGLE_AI -> "projects/${firebaseApp.options.projectId}/templates/"
313+
}
261314
}
262315

263316
/** The [FirebaseAI] instance for the default [FirebaseApp] using the Google AI Backend. */

firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ internal constructor(
233233
}
234234

235235
@OptIn(PublicPreviewAPI::class)
236-
private fun ImagenGenerationResponse.Internal.validate(): ImagenGenerationResponse.Internal {
236+
internal fun ImagenGenerationResponse.Internal.validate(): ImagenGenerationResponse.Internal {
237237
if (predictions.none { it.mimeType != null }) {
238238
throw ContentBlockedException(
239239
message = predictions.first { it.raiFilteredReason != null }.raiFilteredReason
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.ai
18+
19+
import com.google.firebase.FirebaseApp
20+
import com.google.firebase.ai.common.APIController
21+
import com.google.firebase.ai.common.AppCheckHeaderProvider
22+
import com.google.firebase.ai.common.TemplateGenerateContentRequest
23+
import com.google.firebase.ai.type.Content
24+
import com.google.firebase.ai.type.FinishReason
25+
import com.google.firebase.ai.type.FirebaseAIException
26+
import com.google.firebase.ai.type.GenerateContentResponse
27+
import com.google.firebase.ai.type.PromptBlockedException
28+
import com.google.firebase.ai.type.PublicPreviewAPI
29+
import com.google.firebase.ai.type.RequestOptions
30+
import com.google.firebase.ai.type.ResponseStoppedException
31+
import com.google.firebase.ai.type.SerializationException
32+
import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
33+
import com.google.firebase.auth.internal.InternalAuthProvider
34+
import kotlinx.coroutines.flow.Flow
35+
import kotlinx.coroutines.flow.catch
36+
import kotlinx.coroutines.flow.map
37+
import kotlinx.serialization.json.Json
38+
import kotlinx.serialization.json.jsonObject
39+
import org.json.JSONObject
40+
41+
/**
42+
* Represents a multimodal model (like Gemini), capable of generating content based on various
43+
* templated input types.
44+
*/
45+
@PublicPreviewAPI
46+
public class TemplateGenerativeModel
47+
internal constructor(
48+
private val templateUri: String,
49+
private val controller: APIController,
50+
) {
51+
52+
internal constructor(
53+
templateUri: String,
54+
apiKey: String,
55+
firebaseApp: FirebaseApp,
56+
useLimitedUseAppCheckTokens: Boolean,
57+
requestOptions: RequestOptions = RequestOptions(),
58+
appCheckTokenProvider: InteropAppCheckTokenProvider? = null,
59+
internalAuthProvider: InternalAuthProvider? = null
60+
) : this(
61+
templateUri,
62+
APIController(
63+
apiKey,
64+
"",
65+
requestOptions,
66+
"gl-kotlin/${KotlinVersion.CURRENT}-ai fire/${BuildConfig.VERSION_NAME}",
67+
firebaseApp,
68+
AppCheckHeaderProvider(
69+
TAG,
70+
useLimitedUseAppCheckTokens,
71+
appCheckTokenProvider,
72+
internalAuthProvider
73+
),
74+
),
75+
)
76+
77+
/**
78+
* Generates content from a prompt template and inputs.
79+
*
80+
* @param templateId The ID of the prompt template to use.
81+
* @param inputs A map of variables to substitute into the template.
82+
* @return The content generated by the model.
83+
* @throws [FirebaseAIException] if the request failed.
84+
* @see [FirebaseAIException] for types of errors.
85+
*/
86+
public suspend fun generateContent(
87+
templateId: String,
88+
inputs: Map<String, Any>,
89+
): GenerateContentResponse =
90+
try {
91+
controller
92+
.templateGenerateContent("$templateUri$templateId", constructRequest(inputs))
93+
.toPublic()
94+
.validate()
95+
} catch (e: Throwable) {
96+
throw FirebaseAIException.from(e)
97+
}
98+
99+
/**
100+
* Generates content as a stream from a prompt template and inputs.
101+
*
102+
* @param templateId The ID of the prompt template to use.
103+
* @param inputs A map of variables to substitute into the template.
104+
* @return A [Flow] which will emit responses as they are returned by the model.
105+
* @throws [FirebaseAIException] if the request failed.
106+
* @see [FirebaseAIException] for types of errors.
107+
*/
108+
public fun generateContentStream(
109+
templateId: String,
110+
inputs: Map<String, Any>
111+
): Flow<GenerateContentResponse> =
112+
controller
113+
.templateGenerateContentStream("$templateUri$templateId", constructRequest(inputs))
114+
.catch { throw FirebaseAIException.from(it) }
115+
.map { it.toPublic().validate() }
116+
117+
internal fun constructRequest(
118+
inputs: Map<String, Any>,
119+
history: List<Content>? = null
120+
): TemplateGenerateContentRequest {
121+
return TemplateGenerateContentRequest(
122+
Json.parseToJsonElement(JSONObject(inputs).toString()).jsonObject,
123+
history?.let { it.map { it.toTemplateInternal() } }
124+
)
125+
}
126+
127+
private fun GenerateContentResponse.validate() = apply {
128+
if (candidates.isEmpty() && promptFeedback == null) {
129+
throw SerializationException("Error deserializing response, found no valid fields")
130+
}
131+
promptFeedback?.blockReason?.let { throw PromptBlockedException(this) }
132+
candidates
133+
.mapNotNull { it.finishReason }
134+
.firstOrNull { it != FinishReason.STOP }
135+
?.let { throw ResponseStoppedException(this) }
136+
}
137+
138+
private companion object {
139+
private val TAG = TemplateGenerativeModel::class.java.simpleName
140+
}
141+
}

0 commit comments

Comments
 (0)