diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index 1d69c5b0908..a0018bd9fa0 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -13,5 +13,11 @@ * [fixed] Fixed an issue with `LiveContentResponse` audio data not being present when the model was interrupted or the turn completed. (#6870) * [fixed] Fixed an issue with `LiveSession` not converting exceptions to `FirebaseVertexAIException`. (#6870) +* * [changed] **Breaking Change**: Removed the `LiveContentResponse.Status` class, and instead have nested the status + fields as properties of `LiveContentResponse`. (#6906) +* [changed] **Breaking Change**: Removed the `LiveContentResponse` class, and instead have provided subclasses + of `LiveServerMessage` that match the responses from the model. (#6910) +* [feature] Added support for the `id` field on `FunctionResponsePart` and `FunctionCallPart`. (#6910) * [feature] Add support for specifying response modalities in `GenerationConfig`. (#6921) * [feature] Added a helper field for getting all the `InlineDataPart` from a `GenerateContentResponse`. (#6922) + diff --git a/firebase-ai/api.txt b/firebase-ai/api.txt index 10b1cbc965f..d16c1904e6b 100644 --- a/firebase-ai/api.txt +++ b/firebase-ai/api.txt @@ -127,7 +127,7 @@ package com.google.firebase.ai.java { @com.google.firebase.ai.type.PublicPreviewAPI public abstract class LiveSessionFutures { method public abstract com.google.common.util.concurrent.ListenableFuture close(); method public static final com.google.firebase.ai.java.LiveSessionFutures from(com.google.firebase.ai.type.LiveSession session); - method public abstract org.reactivestreams.Publisher receive(); + method public abstract org.reactivestreams.Publisher receive(); method public abstract com.google.common.util.concurrent.ListenableFuture send(com.google.firebase.ai.type.Content content); method public abstract com.google.common.util.concurrent.ListenableFuture send(String text); method public abstract com.google.common.util.concurrent.ListenableFuture sendFunctionResponse(java.util.List functionList); @@ -293,9 +293,12 @@ package com.google.firebase.ai.type { public final class FunctionCallPart implements com.google.firebase.ai.type.Part { ctor public FunctionCallPart(String name, java.util.Map args); + ctor public FunctionCallPart(String name, java.util.Map args, String? id = null); method public java.util.Map getArgs(); + method public String? getId(); method public String getName(); property public final java.util.Map args; + property public final String? id; property public final String name; } @@ -320,8 +323,11 @@ package com.google.firebase.ai.type { public final class FunctionResponsePart implements com.google.firebase.ai.type.Part { ctor public FunctionResponsePart(String name, kotlinx.serialization.json.JsonObject response); + ctor public FunctionResponsePart(String name, kotlinx.serialization.json.JsonObject response, String? id = null); + method public String? getId(); method public String getName(); method public kotlinx.serialization.json.JsonObject getResponse(); + property public final String? id; property public final String name; property public final kotlinx.serialization.json.JsonObject response; } @@ -579,30 +585,6 @@ package com.google.firebase.ai.type { public final class InvalidStateException extends com.google.firebase.ai.type.FirebaseAIException { } - @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveContentResponse { - method public com.google.firebase.ai.type.Content? getData(); - method public java.util.List? getFunctionCalls(); - method public int getStatus(); - method public String? getText(); - property public final com.google.firebase.ai.type.Content? data; - property public final java.util.List? functionCalls; - property public final int status; - property public final String? text; - } - - @kotlin.jvm.JvmInline public static final value class LiveContentResponse.Status { - field public static final com.google.firebase.ai.type.LiveContentResponse.Status.Companion Companion; - } - - public static final class LiveContentResponse.Status.Companion { - method public int getINTERRUPTED(); - method public int getNORMAL(); - method public int getTURN_COMPLETE(); - property public final int INTERRUPTED; - property public final int NORMAL; - property public final int TURN_COMPLETE; - } - @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveGenerationConfig { field public static final com.google.firebase.ai.type.LiveGenerationConfig.Companion Companion; } @@ -638,9 +620,40 @@ package com.google.firebase.ai.type { method public static com.google.firebase.ai.type.LiveGenerationConfig liveGenerationConfig(kotlin.jvm.functions.Function1 init); } + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerContent implements com.google.firebase.ai.type.LiveServerMessage { + ctor public LiveServerContent(com.google.firebase.ai.type.Content? content, boolean interrupted, boolean turnComplete, boolean generationComplete); + method public com.google.firebase.ai.type.Content? getContent(); + method public boolean getGenerationComplete(); + method public boolean getInterrupted(); + method public boolean getTurnComplete(); + property public final com.google.firebase.ai.type.Content? content; + property public final boolean generationComplete; + property public final boolean interrupted; + property public final boolean turnComplete; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public interface LiveServerMessage { + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerSetupComplete implements com.google.firebase.ai.type.LiveServerMessage { + ctor public LiveServerSetupComplete(); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerToolCall implements com.google.firebase.ai.type.LiveServerMessage { + ctor public LiveServerToolCall(java.util.List functionCalls); + method public java.util.List getFunctionCalls(); + property public final java.util.List functionCalls; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerToolCallCancellation implements com.google.firebase.ai.type.LiveServerMessage { + ctor public LiveServerToolCallCancellation(java.util.List functionIds); + method public java.util.List getFunctionIds(); + property public final java.util.List functionIds; + } + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveSession { method public suspend Object? close(kotlin.coroutines.Continuation); - method public kotlinx.coroutines.flow.Flow receive(); + method public kotlinx.coroutines.flow.Flow receive(); method public suspend Object? send(com.google.firebase.ai.type.Content content, kotlin.coroutines.Continuation); method public suspend Object? send(String text, kotlin.coroutines.Continuation); method public suspend Object? sendFunctionResponse(java.util.List functionList, kotlin.coroutines.Continuation); diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt index 3d6652f51d7..1efa2dfedfc 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt @@ -23,7 +23,7 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.FunctionCallPart import com.google.firebase.ai.type.FunctionResponsePart -import com.google.firebase.ai.type.LiveContentResponse +import com.google.firebase.ai.type.LiveServerMessage import com.google.firebase.ai.type.LiveSession import com.google.firebase.ai.type.MediaData import com.google.firebase.ai.type.PublicPreviewAPI @@ -135,16 +135,16 @@ public abstract class LiveSessionFutures internal constructor() { * * Call [close] to stop receiving responses from the model. * - * @return A [Publisher] which will emit [LiveContentResponse] from the model. + * @return A [Publisher] which will emit [LiveServerMessage] from the model. * * @throws [SessionAlreadyReceivingException] when the session is already receiving. * @see stopReceiving */ - public abstract fun receive(): Publisher + public abstract fun receive(): Publisher private class FuturesImpl(private val session: LiveSession) : LiveSessionFutures() { - override fun receive(): Publisher = session.receive().asPublisher() + override fun receive(): Publisher = session.receive().asPublisher() override fun close(): ListenableFuture = SuspendToFutureAdapter.launchFuture { session.close() } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveContentResponse.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveContentResponse.kt deleted file mode 100644 index f06da6057b8..00000000000 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveContentResponse.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.type - -/** - * Represents the response from the model for live content updates. - * - * This class encapsulates the content data, the status of the response, and any function calls - * included in the response. - */ -@PublicPreviewAPI -public class LiveContentResponse -internal constructor( - - /** The main content data of the response. This can be `null` if there is no content. */ - public val data: Content?, - - /** - * The status of the live content response. Indicates whether the response is normal, was - * interrupted, or signifies the completion of a turn. - */ - public val status: Status, - - /** - * A list of [FunctionCallPart] included in the response, if any. - * - * This list can be null or empty if no function calls are present. - */ - public val functionCalls: List? -) { - - /** - * Convenience field representing all the text parts in the response as a single string, if they - * exists. - */ - public val text: String? = - data?.parts?.filterIsInstance()?.joinToString(" ") { it.text } - - /** Represents the status of a [LiveContentResponse], within a single interaction. */ - @JvmInline - public value class Status private constructor(private val value: Int) { - public companion object { - /** The server is actively sending data for the current interaction. */ - public val NORMAL: Status = Status(0) - /** - * The server was interrupted while generating data. - * - * An interruption occurs when the client sends a message while the server is [actively] - * [NORMAL] sending data. - */ - public val INTERRUPTED: Status = Status(1) - /** - * The model has finished sending data in the current interaction. - * - * Can be set alongside content, signifying that the content is the last in the turn. - */ - public val TURN_COMPLETE: Status = Status(2) - } - } -} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt new file mode 100644 index 00000000000..5ab520af474 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt @@ -0,0 +1,190 @@ +/* + * 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.type + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject + +/** + * Parent interface for responses from the model during live interactions. + * + * @see LiveServerContent + * @see LiveServerToolCall + * @see LiveServerToolCallCancellation + * @see LiveServerSetupComplete + */ +@PublicPreviewAPI public interface LiveServerMessage + +/** + * Incremental server update generated by the model in response to client messages. + * + * Content is generated as quickly as possible, and not in realtime. You may choose to buffer and + * play it out in realtime. + */ +@PublicPreviewAPI +public class LiveServerContent( + /** + * The content that the model has generated as part of the current conversation with the user. + * + * This can be `null` if there is no content. + */ + public val content: Content?, + + /** + * The model was interrupted by the client while generating data. + * + * An interruption occurs when the client sends a message while the model is actively sending + * data. + */ + public val interrupted: Boolean, + + /** + * The model has finished sending data in the current turn. + * + * Generation will only start in response to additional client messages. + * + * Can be set alongside [content], indicating that the [content] is the last in the turn. + * + * @see generationComplete + */ + public val turnComplete: Boolean, + + /** + * The model has finished _generating_ data for the current turn. + * + * For realtime playback, there will be a delay between when the model finishes generating content + * and the client has finished playing back the generated content. [generationComplete] indicates + * that the model is done generating data, while [turnComplete] indicates the model is waiting for + * additional client messages. Sending a message during this delay may cause an [interrupted] + * message to be sent. + * + * Note that if the model was [interrupted], this will not be set. The model will go from + * [interrupted] -> [turnComplete]. + */ + public val generationComplete: Boolean, +) : LiveServerMessage { + @OptIn(ExperimentalSerializationApi::class) + @Serializable + internal data class Internal( + val modelTurn: Content.Internal? = null, + val interrupted: Boolean = false, + val turnComplete: Boolean = false, + val generationComplete: Boolean = false + ) + @Serializable + internal data class InternalWrapper(val serverContent: Internal) : InternalLiveServerMessage { + @OptIn(ExperimentalSerializationApi::class) + override fun toPublic() = + LiveServerContent( + serverContent.modelTurn?.toPublic(), + serverContent.interrupted, + serverContent.turnComplete, + serverContent.generationComplete + ) + } +} + +/** The model is ready to receive client messages. */ +@PublicPreviewAPI +public class LiveServerSetupComplete : LiveServerMessage { + @Serializable + internal data class Internal(val setupComplete: JsonObject) : InternalLiveServerMessage { + override fun toPublic() = LiveServerSetupComplete() + } +} + +/** + * Request for the client to execute the provided [functionCalls]. + * + * The client should return matching [FunctionResponsePart], where the `id` fields correspond to + * individual [FunctionCallPart]s. + * + * @property functionCalls A list of [FunctionCallPart] to run and return responses for. + */ +@PublicPreviewAPI +public class LiveServerToolCall(public val functionCalls: List) : + LiveServerMessage { + @Serializable + internal data class Internal( + val functionCalls: List = emptyList() + ) + @Serializable + internal data class InternalWrapper(val toolCall: Internal) : InternalLiveServerMessage { + override fun toPublic() = + LiveServerToolCall( + toolCall.functionCalls.map { functionCall -> + FunctionCallPart( + name = functionCall.name, + args = functionCall.args.orEmpty().mapValues { it.value ?: JsonNull } + ) + } + ) + } +} + +/** + * Notification for the client to cancel a previous function call from [LiveServerToolCall]. + * + * You do not need to send [FunctionResponsePart]s for the cancelled [FunctionCallPart]s. + * + * @property functionIds A list of `id`s matching the `id` provided in a previous + * [LiveServerToolCall], where only the provided `id`s should be cancelled. + */ +@PublicPreviewAPI +public class LiveServerToolCallCancellation(public val functionIds: List) : + LiveServerMessage { + @Serializable internal data class Internal(val functionIds: List = emptyList()) + @Serializable + internal data class InternalWrapper(val toolCallCancellation: Internal) : + InternalLiveServerMessage { + override fun toPublic() = LiveServerToolCallCancellation(toolCallCancellation.functionIds) + } +} + +@PublicPreviewAPI +@Serializable(LiveServerMessageSerializer::class) +internal sealed interface InternalLiveServerMessage { + fun toPublic(): LiveServerMessage +} + +@OptIn(PublicPreviewAPI::class) +internal object LiveServerMessageSerializer : + JsonContentPolymorphicSerializer(InternalLiveServerMessage::class) { + @OptIn(PublicPreviewAPI::class) + override fun selectDeserializer( + element: JsonElement + ): DeserializationStrategy { + val jsonObject = element.jsonObject + return when { + "serverContent" in jsonObject -> LiveServerContent.InternalWrapper.serializer() + "setupComplete" in jsonObject -> LiveServerSetupComplete.Internal.serializer() + "toolCall" in jsonObject -> LiveServerToolCall.InternalWrapper.serializer() + "toolCallCancellation" in jsonObject -> + LiveServerToolCallCancellation.InternalWrapper.serializer() + else -> + throw SerializationException( + "The given subclass of LiveServerMessage (${javaClass.simpleName}) is not supported in the serialization yet." + ) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt index 74c8669ad2f..1f84c18a53b 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt @@ -43,7 +43,6 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.transform import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.yield @@ -51,9 +50,6 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.decodeFromJsonElement /** Represents a live WebSocket session capable of streaming content to and from the server. */ @PublicPreviewAPI @@ -140,12 +136,12 @@ internal constructor( * * Call [close] to stop receiving responses from the model. * - * @return A [Flow] which will emit [LiveContentResponse] from the model. + * @return A [Flow] which will emit [LiveServerMessage] from the model. * * @throws [SessionAlreadyReceivingException] when the session is already receiving. * @see stopReceiving */ - public fun receive(): Flow { + public fun receive(): Flow { return FirebaseAIException.catch { if (startedReceiving.getAndSet(true)) { throw SessionAlreadyReceivingException() @@ -157,8 +153,14 @@ internal constructor( val response = session.incoming.tryReceive() if (response.isClosed || !startedReceiving.get()) break - val frame = response.getOrNull() - frame?.let { frameToLiveContentResponse(it) }?.let { emit(it) } + response + .getOrNull() + ?.let { + JSON.decodeFromString( + it.readBytes().toString(Charsets.UTF_8) + ) + } + ?.let { emit(it.toPublic()) } yield() } @@ -167,12 +169,6 @@ internal constructor( .catch { throw FirebaseAIException.from(it) } // TODO(b/410059569): Add back when fixed - // return session.incoming.receiveAsFlow().transform { frame -> - // val response = frameToLiveContentResponse(frame) - // response?.let { emit(it) } - // }.onCompletion { - // stopAudioConversation() - // }.catch { throw FirebaseVertexAIException.from(it) } } } @@ -309,30 +305,48 @@ internal constructor( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? ) { receive() - .transform { - if (it.status == LiveContentResponse.Status.INTERRUPTED) { - playBackQueue.clear() - } else { - emit(it) - } - } .onEach { - if (!it.functionCalls.isNullOrEmpty()) { - if (functionCallHandler != null) { - // It's fine to suspend here since you can't have a function call running concurrently - // with an audio response - sendFunctionResponse(it.functionCalls.map(functionCallHandler).toList()) - } else { + when (it) { + is LiveServerToolCall -> { + if (it.functionCalls.isEmpty()) { + Log.w( + TAG, + "The model sent a tool call request, but it was missing functions to call." + ) + } else if (functionCallHandler != null) { + // It's fine to suspend here since you can't have a function call running concurrently + // with an audio response + sendFunctionResponse(it.functionCalls.map(functionCallHandler).toList()) + } else { + Log.w( + TAG, + "Function calls were present in the response, but a functionCallHandler was not provided." + ) + } + } + is LiveServerToolCallCancellation -> { Log.w( TAG, - "Function calls were present in the response, but a functionCallHandler was not provided." + "The model sent a tool cancellation request, but tool cancellation is not supported when using startAudioConversation()." + ) + } + is LiveServerContent -> { + if (it.interrupted) { + playBackQueue.clear() + } else { + val audioParts = it.content?.parts?.filterIsInstance().orEmpty() + for (part in audioParts) { + playBackQueue.add(part.inlineData) + } + } + } + is LiveServerSetupComplete -> { + // we should only get this message when we initially `connect` in LiveGenerativeModel + Log.w( + TAG, + "The model sent LiveServerSetupComplete after the connection was established." ) } - } - - val audioParts = it.data?.parts?.filterIsInstance().orEmpty() - for (part in audioParts) { - playBackQueue.add(part.inlineData) } } .launchIn(scope) @@ -368,50 +382,6 @@ internal constructor( } } - /** - * Converts a [Frame] from the model to a valid [LiveContentResponse], if possible. - * - * @return The corresponding [LiveContentResponse] or null if it couldn't be converted. - */ - private fun frameToLiveContentResponse(frame: Frame): LiveContentResponse? { - val jsonMessage = Json.parseToJsonElement(frame.readBytes().toString(Charsets.UTF_8)) - - if (jsonMessage !is JsonObject) { - Log.w(TAG, "Server response was not a JsonObject: $jsonMessage") - return null - } - - return when { - "toolCall" in jsonMessage -> { - val functionContent = - JSON.decodeFromJsonElement(jsonMessage) - LiveContentResponse( - null, - LiveContentResponse.Status.NORMAL, - functionContent.toolCall.functionCalls.map { - FunctionCallPart(it.name, it.args.orEmpty().mapValues { x -> x.value ?: JsonNull }) - } - ) - } - "serverContent" in jsonMessage -> { - val serverContent = - JSON.decodeFromJsonElement(jsonMessage) - .serverContent - val status = - when { - serverContent.turnComplete == true -> LiveContentResponse.Status.TURN_COMPLETE - serverContent.interrupted == true -> LiveContentResponse.Status.INTERRUPTED - else -> LiveContentResponse.Status.NORMAL - } - LiveContentResponse(serverContent.modelTurn?.toPublic(), status, null) - } - else -> { - Log.w(TAG, "Failed to decode the server response: $jsonMessage") - null - } - } - } - /** * Incremental update of the current conversation delivered from the client. * @@ -433,51 +403,7 @@ internal constructor( fun toInternal() = Internal(Internal.BidiGenerateContentClientContent(turns, turnComplete)) } - /** - * Incremental server update generated by the model in response to client messages. - * - * Effectively, a message from the model to the client. - */ - internal class BidiGenerateContentServerContentSetup( - val modelTurn: Content.Internal?, - val turnComplete: Boolean?, - val interrupted: Boolean? - ) { - @Serializable - internal class Internal(val serverContent: BidiGenerateContentServerContent) { - @Serializable - internal data class BidiGenerateContentServerContent( - val modelTurn: Content.Internal?, - val turnComplete: Boolean?, - val interrupted: Boolean? - ) - } - - fun toInternal() = - Internal(Internal.BidiGenerateContentServerContent(modelTurn, turnComplete, interrupted)) - } - - /** - * Request for the client to execute the provided function calls and return the responses with the - * matched `id`s. - */ - internal data class BidiGenerateContentToolCallSetup( - val functionCalls: List - ) { - @Serializable - internal class Internal(val toolCall: BidiGenerateContentToolCall) { - @Serializable - internal data class BidiGenerateContentToolCall( - val functionCalls: List - ) - } - - fun toInternal(): Internal { - return Internal(Internal.BidiGenerateContentToolCall(functionCalls)) - } - } - - /** Client generated responses to a [BidiGenerateContentToolCallSetup]. */ + /** Client generated responses to a [LiveServerToolCall]. */ internal class BidiGenerateContentToolResponseSetup( val functionResponses: List ) { diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt index 4d1a8297e0c..bcc7e14b657 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt @@ -77,38 +77,57 @@ public class InlineDataPart(public val inlineData: ByteArray, public val mimeTyp * * @param name the name of the function to call * @param args the function parameters and values as a [Map] + * @param id Unique id of the function call. If present, the returned [FunctionResponsePart] should + * have a matching `id` field. */ -// TODO(b/410040441): Support id property -public class FunctionCallPart( +public class FunctionCallPart +@JvmOverloads +constructor( public val name: String, public val args: Map, + public val id: String? = null ) : Part { @Serializable internal data class Internal(val functionCall: FunctionCall) : InternalPart { @Serializable - internal data class FunctionCall(val name: String, val args: Map? = null) + internal data class FunctionCall( + val name: String, + val args: Map? = null, + val id: String? = null + ) } } /** * Represents function call output to be returned to the model when it requests a function call. * - * @param name the name of the called function - * @param response the response produced by the function as a [JSONObject] + * @param name The name of the called function. + * @param response The response produced by the function as a [JSONObject]. + * @param id Matching `id` for a [FunctionCallPart], if one was provided. */ -// TODO(b/410040441): Support id property -public class FunctionResponsePart(public val name: String, public val response: JsonObject) : Part { +public class FunctionResponsePart +@JvmOverloads +constructor( + public val name: String, + public val response: JsonObject, + public val id: String? = null +) : Part { @Serializable internal data class Internal(val functionResponse: FunctionResponse) : InternalPart { - @Serializable internal data class FunctionResponse(val name: String, val response: JsonObject) + @Serializable + internal data class FunctionResponse( + val name: String, + val response: JsonObject, + val id: String? = null + ) } internal fun toInternalFunctionCall(): Internal.FunctionResponse { - return Internal.FunctionResponse(this.name, this.response) + return Internal.FunctionResponse(name, response, id) } } @@ -181,9 +200,11 @@ internal fun Part.toInternal(): InternalPart { ) ) is FunctionCallPart -> - FunctionCallPart.Internal(FunctionCallPart.Internal.FunctionCall(name, args)) + FunctionCallPart.Internal(FunctionCallPart.Internal.FunctionCall(name, args, id)) is FunctionResponsePart -> - FunctionResponsePart.Internal(FunctionResponsePart.Internal.FunctionResponse(name, response)) + FunctionResponsePart.Internal( + FunctionResponsePart.Internal.FunctionResponse(name, response, id) + ) is FileDataPart -> FileDataPart.Internal(FileDataPart.Internal.FileData(mimeType = mimeType, fileUri = uri)) else -> @@ -214,13 +235,11 @@ internal fun InternalPart.toPublic(): Part { is FunctionCallPart.Internal -> FunctionCallPart( functionCall.name, - functionCall.args.orEmpty().mapValues { it.value ?: JsonNull } + functionCall.args.orEmpty().mapValues { it.value ?: JsonNull }, + functionCall.id ) is FunctionResponsePart.Internal -> - FunctionResponsePart( - functionResponse.name, - functionResponse.response, - ) + FunctionResponsePart(functionResponse.name, functionResponse.response, functionResponse.id) is FileDataPart.Internal -> FileDataPart(fileData.mimeType, fileData.fileUri) else -> throw com.google.firebase.ai.type.SerializationException(