Skip to content
57 changes: 57 additions & 0 deletions firebase-ai/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ package com.google.firebase.ai {
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);
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);
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());
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.TemplateGenerativeModel templateGenerativeModel();
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());
method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.TemplateImagenModel templateImagenModel();
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());
property public static final com.google.firebase.ai.FirebaseAI instance;
field public static final com.google.firebase.ai.FirebaseAI.Companion Companion;
}
Expand Down Expand Up @@ -83,6 +87,24 @@ package com.google.firebase.ai {
method public suspend Object? connect(kotlin.coroutines.Continuation<? super com.google.firebase.ai.type.LiveSession>);
}

@com.google.firebase.ai.type.PublicPreviewAPI public final class TemplateChat {
ctor public TemplateChat(com.google.firebase.ai.TemplateGenerativeModel model, String templateID, java.util.List<com.google.firebase.ai.type.Content> history = java.util.ArrayList());
method public java.util.List<com.google.firebase.ai.type.Content> getHistory();
method public suspend Object? sendMessage(com.google.firebase.ai.type.Content prompt, java.util.Map<java.lang.String,?> inputs, kotlin.coroutines.Continuation<? super com.google.firebase.ai.type.GenerateContentResponse>);
method public kotlinx.coroutines.flow.Flow<com.google.firebase.ai.type.GenerateContentResponse> sendMessageStream(com.google.firebase.ai.type.Content prompt, java.util.Map<java.lang.String,?> inputs);
property public final java.util.List<com.google.firebase.ai.type.Content> history;
}

@com.google.firebase.ai.type.PublicPreviewAPI public final class TemplateGenerativeModel {
method public suspend Object? generateContent(String templateId, java.util.Map<java.lang.String,?> inputs, java.util.List<com.google.firebase.ai.type.Content>? history = null, kotlin.coroutines.Continuation<? super com.google.firebase.ai.type.GenerateContentResponse>);
method public kotlinx.coroutines.flow.Flow<com.google.firebase.ai.type.GenerateContentResponse> generateContentStream(String templateId, java.util.Map<java.lang.String,?> inputs, java.util.List<com.google.firebase.ai.type.Content>? history = null);
method public com.google.firebase.ai.TemplateChat startChat(String templateId, java.util.List<com.google.firebase.ai.type.Content> history = emptyList());
}

@com.google.firebase.ai.type.PublicPreviewAPI public final class TemplateImagenModel {
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>>);
}

}

package com.google.firebase.ai.java {
Expand Down Expand Up @@ -166,6 +188,41 @@ package com.google.firebase.ai.java {
method public com.google.firebase.ai.java.LiveSessionFutures from(com.google.firebase.ai.type.LiveSession session);
}

@com.google.firebase.ai.type.PublicPreviewAPI public abstract class TemplateChatFutures {
method public static final com.google.firebase.ai.java.TemplateChatFutures from(com.google.firebase.ai.TemplateChat chat);
method public abstract com.google.firebase.ai.TemplateChat getChat();
method public abstract com.google.common.util.concurrent.ListenableFuture<com.google.firebase.ai.type.GenerateContentResponse> sendMessage(com.google.firebase.ai.type.Content prompt, java.util.Map<java.lang.String,?> inputs);
method public abstract org.reactivestreams.Publisher<com.google.firebase.ai.type.GenerateContentResponse> sendMessageStream(com.google.firebase.ai.type.Content prompt, java.util.Map<java.lang.String,?> inputs);
field public static final com.google.firebase.ai.java.TemplateChatFutures.Companion Companion;
}

public static final class TemplateChatFutures.Companion {
method public com.google.firebase.ai.java.TemplateChatFutures from(com.google.firebase.ai.TemplateChat chat);
}

public abstract class TemplateGenerativeModelFutures {
method public static final com.google.firebase.ai.java.TemplateGenerativeModelFutures from(com.google.firebase.ai.TemplateGenerativeModel model);
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);
method public abstract org.reactivestreams.Publisher<com.google.firebase.ai.type.GenerateContentResponse> generateContentStream(String templateId, java.util.Map<java.lang.String,?> inputs);
method public abstract com.google.firebase.ai.TemplateGenerativeModel getGenerativeModel();
field public static final com.google.firebase.ai.java.TemplateGenerativeModelFutures.Companion Companion;
}

public static final class TemplateGenerativeModelFutures.Companion {
method public com.google.firebase.ai.java.TemplateGenerativeModelFutures from(com.google.firebase.ai.TemplateGenerativeModel model);
}

public abstract class TemplateImagenModelFutures {
method public static final com.google.firebase.ai.java.TemplateImagenModelFutures from(com.google.firebase.ai.TemplateImagenModel model);
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);
method public abstract com.google.firebase.ai.TemplateImagenModel getImageModel();
field public static final com.google.firebase.ai.java.TemplateImagenModelFutures.Companion Companion;
}

public static final class TemplateImagenModelFutures.Companion {
method public com.google.firebase.ai.java.TemplateImagenModelFutures from(com.google.firebase.ai.TemplateImagenModel model);
}

}

package com.google.firebase.ai.type {
Expand Down
53 changes: 53 additions & 0 deletions firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ internal constructor(
)
}

/**
* Instantiates a new [TemplateGenerativeModel] given the provided parameters.
*
* @param requestOptions Configuration options for sending requests to the backend.
* @return The initialized [TemplateGenerativeModel] instance.
*/
@JvmOverloads
@PublicPreviewAPI
public fun templateGenerativeModel(
requestOptions: RequestOptions = RequestOptions(),
): TemplateGenerativeModel {
val templateUri = getTemplateUri(backend)
return TemplateGenerativeModel(
templateUri,
firebaseApp.options.apiKey,
firebaseApp,
useLimitedUseAppCheckTokens,
requestOptions,
appCheckProvider.get(),
internalAuthProvider.get(),
)
}

/**
* Instantiates a new [LiveGenerationConfig] given the provided parameters.
*
Expand Down Expand Up @@ -205,6 +228,29 @@ internal constructor(
)
}

/**
* Instantiates a new [TemplateImagenModel] given the provided parameters.
*
* @param requestOptions Configuration options for sending requests to the backend.
* @return The initialized [TemplateImagenModel] instance.
*/
@JvmOverloads
@PublicPreviewAPI
public fun templateImagenModel(
requestOptions: RequestOptions = RequestOptions(),
): TemplateImagenModel {
val templateUri = getTemplateUri(backend)
return TemplateImagenModel(
templateUri,
firebaseApp.options.apiKey,
firebaseApp,
useLimitedUseAppCheckTokens,
requestOptions,
appCheckProvider.get(),
internalAuthProvider.get(),
)
}

public companion object {
/** The [FirebaseAI] instance for the default [FirebaseApp] using the Google AI Backend. */
@JvmStatic
Expand Down Expand Up @@ -258,6 +304,13 @@ internal constructor(

private val TAG = FirebaseAI::class.java.simpleName
}

private fun getTemplateUri(backend: GenerativeBackend): String =
when (backend.backend) {
GenerativeBackendEnum.VERTEX_AI ->
"projects/${firebaseApp.options.projectId}/locations/${backend.location}/templates/"
GenerativeBackendEnum.GOOGLE_AI -> "projects/${firebaseApp.options.projectId}/templates/"
}
}

/** The [FirebaseAI] instance for the default [FirebaseApp] using the Google AI Backend. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ internal constructor(
}

@OptIn(PublicPreviewAPI::class)
private fun ImagenGenerationResponse.Internal.validate(): ImagenGenerationResponse.Internal {
internal fun ImagenGenerationResponse.Internal.validate(): ImagenGenerationResponse.Internal {
if (predictions.none { it.mimeType != null }) {
throw ContentBlockedException(
message = predictions.first { it.raiFilteredReason != null }.raiFilteredReason
Expand Down
166 changes: 166 additions & 0 deletions firebase-ai/src/main/kotlin/com/google/firebase/ai/TemplateChat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.ai

import android.graphics.Bitmap
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerateContentResponse
import com.google.firebase.ai.type.ImagePart
import com.google.firebase.ai.type.InlineDataPart
import com.google.firebase.ai.type.InvalidStateException
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.TextPart
import com.google.firebase.ai.type.content
import java.util.LinkedList
import java.util.concurrent.Semaphore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach

/**
* Representation of a multi-turn interaction with a model.
*
* Captures and stores the history of communication in memory, and provides it as context with each
* new message.
*
* **Note:** This object is not thread-safe, and calling [sendMessage] multiple times without
* waiting for a response will throw an [InvalidStateException].
*
* @param model The model to use for the interaction.
* @param templateID The template ID for this chat session
* @property history The previous content from the chat that has been successfully sent and received
* from the model. This will be provided to the model for each message sent (as context for the
* discussion).
*/
@PublicPreviewAPI
public class TemplateChat(
private val model: TemplateGenerativeModel,
private val templateID: String,
public val history: MutableList<Content> = ArrayList()
) {
private var lock = Semaphore(1)

/**
* Sends a message using the provided [prompt]; automatically providing the existing [history] as
* context.
*
* If successful, the message and response will be added to the [history]. If unsuccessful,
* [history] will remain unchanged.
*
* @param prompt The input that, together with the history, will be given to the model as the
* prompt.
* @param inputs the inputs needed to fill in the template ID
* @throws InvalidStateException if [prompt] is not coming from the 'user' role.
* @throws InvalidStateException if the [Chat] instance has an active request.
*/
public suspend fun sendMessage(
prompt: Content,
inputs: Map<String, Any>
): GenerateContentResponse {
prompt.assertComesFromUser()
attemptLock()
try {
val fullPrompt = history + prompt
val response = model.generateContent(templateID, inputs, fullPrompt)
history.add(prompt)
history.add(response.candidates.first().content)
return response
} finally {
lock.release()
}
}

/**
* Sends a message using the existing history of this chat as context and the provided [Content]
* prompt.
*
* The response from the model is returned as a stream.
*
* If successful, the message and response will be added to the history. If unsuccessful, history
* will remain unchanged.
*
* @param prompt The input that, together with the history, will be given to the model as the
* prompt.
* @param inputs the inputs needed to fill in the template ID
* @throws InvalidStateException if [prompt] is not coming from the 'user' role.
* @throws InvalidStateException if the [Chat] instance has an active request.
*/
public fun sendMessageStream(
prompt: Content,
inputs: Map<String, Any>
): Flow<GenerateContentResponse> {
prompt.assertComesFromUser()
attemptLock()

val fullPrompt = history + prompt
val flow = model.generateContentStream(templateID, inputs, fullPrompt)
val bitmaps = LinkedList<Bitmap>()
val inlineDataParts = LinkedList<InlineDataPart>()
val text = StringBuilder()

/**
* TODO: revisit when images and inline data are returned. This will cause issues with how
* things are structured in the response. eg; a text/image/text response will be (incorrectly)
* represented as image/text
*/
return flow
.onEach {
for (part in it.candidates.first().content.parts) {
when (part) {
is TextPart -> text.append(part.text)
is ImagePart -> bitmaps.add(part.image)
is InlineDataPart -> inlineDataParts.add(part)
}
}
}
.onCompletion {
lock.release()
if (it == null) {
val content =
content("model") {
for (bitmap in bitmaps) {
image(bitmap)
}
for (inlineDataPart in inlineDataParts) {
inlineData(inlineDataPart.inlineData, inlineDataPart.mimeType)
}
if (text.isNotBlank()) {
text(text.toString())
}
}

history.add(prompt)
history.add(content)
}
}
}

private fun Content.assertComesFromUser() {
if (role !in listOf("user", "function")) {
throw InvalidStateException("Chat prompts should come from the 'user' or 'function' role.")
}
}

private fun attemptLock() {
if (!lock.tryAcquire()) {
throw InvalidStateException(
"This chat instance currently has an ongoing request, please wait for it to complete " +
"before sending more messages"
)
}
}
}
Loading
Loading