diff --git a/ai-logic/firebase-ai/CHANGELOG.md b/ai-logic/firebase-ai/CHANGELOG.md index 443f0cdbc66..d6d3d370d13 100644 --- a/ai-logic/firebase-ai/CHANGELOG.md +++ b/ai-logic/firebase-ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [feature] Added static factory methods `create` for `Part` implementations to expose `thoughtSignature` and `isThought` properties. (#8352) + # 17.13.0 - [feature] Expanded `SpeechConfig` to support `MultiSpeakerVoiceConfig` and `LanguageCode`. diff --git a/ai-logic/firebase-ai/api.txt b/ai-logic/firebase-ai/api.txt index a0e530b6d27..8f1b4a02a4e 100644 --- a/ai-logic/firebase-ai/api.txt +++ b/ai-logic/firebase-ai/api.txt @@ -466,13 +466,25 @@ package com.google.firebase.ai.type { public final class CodeExecutionResultPart implements com.google.firebase.ai.type.Part { ctor @Deprecated public CodeExecutionResultPart(String outcome, String output); + method @Deprecated public static com.google.firebase.ai.type.CodeExecutionResultPart create(String outcome, String output); + method @Deprecated public static com.google.firebase.ai.type.CodeExecutionResultPart create(String outcome, String output, boolean isThought = false); + method @Deprecated public static com.google.firebase.ai.type.CodeExecutionResultPart create(String outcome, String output, boolean isThought = false, String? thoughtSignature = null); method public boolean executionSucceeded(); method public String getOutcome(); method public String getOutput(); + method public String? getThoughtSignature(); method public boolean isThought(); property public boolean isThought; property public final String outcome; property public final String output; + property public final String? thoughtSignature; + field public static final com.google.firebase.ai.type.CodeExecutionResultPart.Companion Companion; + } + + public static final class CodeExecutionResultPart.Companion { + method @Deprecated public com.google.firebase.ai.type.CodeExecutionResultPart create(String outcome, String output); + method @Deprecated public com.google.firebase.ai.type.CodeExecutionResultPart create(String outcome, String output, boolean isThought = false); + method @Deprecated public com.google.firebase.ai.type.CodeExecutionResultPart create(String outcome, String output, boolean isThought = false, String? thoughtSignature = null); } public final class Content { @@ -552,22 +564,46 @@ package com.google.firebase.ai.type { public final class ExecutableCodePart implements com.google.firebase.ai.type.Part { ctor @Deprecated public ExecutableCodePart(String language, String code); + method @Deprecated public static com.google.firebase.ai.type.ExecutableCodePart create(String language, String code); + method @Deprecated public static com.google.firebase.ai.type.ExecutableCodePart create(String language, String code, boolean isThought = false); + method @Deprecated public static com.google.firebase.ai.type.ExecutableCodePart create(String language, String code, boolean isThought = false, String? thoughtSignature = null); method public String getCode(); method public String getLanguage(); + method public String? getThoughtSignature(); method public boolean isThought(); property public final String code; property public boolean isThought; property public final String language; + property public final String? thoughtSignature; + field public static final com.google.firebase.ai.type.ExecutableCodePart.Companion Companion; + } + + public static final class ExecutableCodePart.Companion { + method @Deprecated public com.google.firebase.ai.type.ExecutableCodePart create(String language, String code); + method @Deprecated public com.google.firebase.ai.type.ExecutableCodePart create(String language, String code, boolean isThought = false); + method @Deprecated public com.google.firebase.ai.type.ExecutableCodePart create(String language, String code, boolean isThought = false, String? thoughtSignature = null); } public final class FileDataPart implements com.google.firebase.ai.type.Part { ctor public FileDataPart(String uri, String mimeType); + method public static com.google.firebase.ai.type.FileDataPart create(String uri, String mimeType); + method public static com.google.firebase.ai.type.FileDataPart create(String uri, String mimeType, boolean isThought = false); + method public static com.google.firebase.ai.type.FileDataPart create(String uri, String mimeType, boolean isThought = false, String? thoughtSignature = null); method public String getMimeType(); + method public String? getThoughtSignature(); method public String getUri(); method public boolean isThought(); property public boolean isThought; property public final String mimeType; + property public final String? thoughtSignature; property public final String uri; + field public static final com.google.firebase.ai.type.FileDataPart.Companion Companion; + } + + public static final class FileDataPart.Companion { + method public com.google.firebase.ai.type.FileDataPart create(String uri, String mimeType); + method public com.google.firebase.ai.type.FileDataPart create(String uri, String mimeType, boolean isThought = false); + method public com.google.firebase.ai.type.FileDataPart create(String uri, String mimeType, boolean isThought = false, String? thoughtSignature = null); } public final class FinishReason { @@ -623,14 +659,28 @@ 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 static com.google.firebase.ai.type.FunctionCallPart create(String name, java.util.Map args); + method public static com.google.firebase.ai.type.FunctionCallPart create(String name, java.util.Map args, String? id = null); + method public static com.google.firebase.ai.type.FunctionCallPart create(String name, java.util.Map args, String? id = null, boolean isThought = false); + method public static com.google.firebase.ai.type.FunctionCallPart create(String name, java.util.Map args, String? id = null, boolean isThought = false, String? thoughtSignature = null); method public java.util.Map getArgs(); method public String? getId(); method public String getName(); + method public String? getThoughtSignature(); method public boolean isThought(); property public final java.util.Map args; property public final String? id; property public boolean isThought; property public final String name; + property public final String? thoughtSignature; + field public static final com.google.firebase.ai.type.FunctionCallPart.Companion Companion; + } + + public static final class FunctionCallPart.Companion { + method public com.google.firebase.ai.type.FunctionCallPart create(String name, java.util.Map args); + method public com.google.firebase.ai.type.FunctionCallPart create(String name, java.util.Map args, String? id = null); + method public com.google.firebase.ai.type.FunctionCallPart create(String name, java.util.Map args, String? id = null, boolean isThought = false); + method public com.google.firebase.ai.type.FunctionCallPart create(String name, java.util.Map args, String? id = null, boolean isThought = false, String? thoughtSignature = null); } public final class FunctionCallingConfig { @@ -656,20 +706,32 @@ package com.google.firebase.ai.type { ctor public FunctionResponsePart(String name, kotlinx.serialization.json.JsonObject response); ctor public FunctionResponsePart(String name, kotlinx.serialization.json.JsonObject response, String? id = null); ctor public FunctionResponsePart(String name, kotlinx.serialization.json.JsonObject response, String? id = null, java.util.List parts = emptyList()); + method public static com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response); + method public static com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response, String? id = null); + method public static com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response, String? id = null, java.util.List parts = emptyList()); + method public static com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response, String? id = null, java.util.List parts = emptyList(), boolean isThought = false); + method public static com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response, String? id = null, java.util.List parts = emptyList(), boolean isThought = false, String? thoughtSignature = null); method public String? getId(); method public String getName(); method public java.util.List getParts(); method public kotlinx.serialization.json.JsonObject getResponse(); + method public String? getThoughtSignature(); method public boolean isThought(); property public final String? id; property public boolean isThought; property public final String name; property public final java.util.List parts; property public final kotlinx.serialization.json.JsonObject response; + property public final String? thoughtSignature; field public static final com.google.firebase.ai.type.FunctionResponsePart.Companion Companion; } public static final class FunctionResponsePart.Companion { + method public com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response); + method public com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response, String? id = null); + method public com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response, String? id = null, java.util.List parts = emptyList()); + method public com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response, String? id = null, java.util.List parts = emptyList(), boolean isThought = false); + method public com.google.firebase.ai.type.FunctionResponsePart create(String name, kotlinx.serialization.json.JsonObject response, String? id = null, java.util.List parts = emptyList(), boolean isThought = false, String? thoughtSignature = null); method public com.google.firebase.ai.type.FunctionResponsePart from(kotlinx.serialization.json.JsonObject jsonObject, java.util.List parts = emptyList()); } @@ -920,12 +982,26 @@ package com.google.firebase.ai.type { public final class ImagePart implements com.google.firebase.ai.type.Part { ctor public ImagePart(android.graphics.Bitmap image); ctor public ImagePart(android.graphics.Bitmap image, String displayName); + method public static com.google.firebase.ai.type.ImagePart create(android.graphics.Bitmap image); + method public static com.google.firebase.ai.type.ImagePart create(android.graphics.Bitmap image, String? displayName = null); + method public static com.google.firebase.ai.type.ImagePart create(android.graphics.Bitmap image, String? displayName = null, boolean isThought = false); + method public static com.google.firebase.ai.type.ImagePart create(android.graphics.Bitmap image, String? displayName = null, boolean isThought = false, String? thoughtSignature = null); method public String? getDisplayName(); method public android.graphics.Bitmap getImage(); + method public String? getThoughtSignature(); method public boolean isThought(); property public final String? displayName; property public final android.graphics.Bitmap image; property public boolean isThought; + property public final String? thoughtSignature; + field public static final com.google.firebase.ai.type.ImagePart.Companion Companion; + } + + public static final class ImagePart.Companion { + method public com.google.firebase.ai.type.ImagePart create(android.graphics.Bitmap image); + method public com.google.firebase.ai.type.ImagePart create(android.graphics.Bitmap image, String? displayName = null); + method public com.google.firebase.ai.type.ImagePart create(android.graphics.Bitmap image, String? displayName = null, boolean isThought = false); + method public com.google.firebase.ai.type.ImagePart create(android.graphics.Bitmap image, String? displayName = null, boolean isThought = false, String? thoughtSignature = null); } public final class ImageSize { @@ -1171,14 +1247,28 @@ package com.google.firebase.ai.type { public final class InlineDataPart implements com.google.firebase.ai.type.Part { ctor public InlineDataPart(byte[] inlineData, String mimeType); ctor public InlineDataPart(byte[] inlineData, String mimeType, String displayName); + method public static com.google.firebase.ai.type.InlineDataPart create(byte[] inlineData, String mimeType); + method public static com.google.firebase.ai.type.InlineDataPart create(byte[] inlineData, String mimeType, String? displayName = null); + method public static com.google.firebase.ai.type.InlineDataPart create(byte[] inlineData, String mimeType, String? displayName = null, boolean isThought = false); + method public static com.google.firebase.ai.type.InlineDataPart create(byte[] inlineData, String mimeType, String? displayName = null, boolean isThought = false, String? thoughtSignature = null); method public String? getDisplayName(); method public byte[] getInlineData(); method public String getMimeType(); + method public String? getThoughtSignature(); method public boolean isThought(); property public final String? displayName; property public final byte[] inlineData; property public boolean isThought; property public final String mimeType; + property public final String? thoughtSignature; + field public static final com.google.firebase.ai.type.InlineDataPart.Companion Companion; + } + + public static final class InlineDataPart.Companion { + method public com.google.firebase.ai.type.InlineDataPart create(byte[] inlineData, String mimeType); + method public com.google.firebase.ai.type.InlineDataPart create(byte[] inlineData, String mimeType, String? displayName = null); + method public com.google.firebase.ai.type.InlineDataPart create(byte[] inlineData, String mimeType, String? displayName = null, boolean isThought = false); + method public com.google.firebase.ai.type.InlineDataPart create(byte[] inlineData, String mimeType, String? displayName = null, boolean isThought = false, String? thoughtSignature = null); } public final class InvalidAPIKeyException extends com.google.firebase.ai.type.FirebaseAIException { @@ -1882,10 +1972,22 @@ package com.google.firebase.ai.type { public final class TextPart implements com.google.firebase.ai.type.Part { ctor public TextPart(String text); + method public static com.google.firebase.ai.type.TextPart create(String text); + method public static com.google.firebase.ai.type.TextPart create(String text, boolean isThought = false); + method public static com.google.firebase.ai.type.TextPart create(String text, boolean isThought = false, String? thoughtSignature = null); method public String getText(); + method public String? getThoughtSignature(); method public boolean isThought(); property public boolean isThought; property public final String text; + property public final String? thoughtSignature; + field public static final com.google.firebase.ai.type.TextPart.Companion Companion; + } + + public static final class TextPart.Companion { + method public com.google.firebase.ai.type.TextPart create(String text); + method public com.google.firebase.ai.type.TextPart create(String text, boolean isThought = false); + method public com.google.firebase.ai.type.TextPart create(String text, boolean isThought = false, String? thoughtSignature = null); } public final class ThinkingConfig { diff --git a/ai-logic/firebase-ai/gradle.properties b/ai-logic/firebase-ai/gradle.properties index fc768a5c821..d3e3593ddbd 100644 --- a/ai-logic/firebase-ai/gradle.properties +++ b/ai-logic/firebase-ai/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=17.13.1 +version=17.14.0 latestReleasedVersion=17.13.0 diff --git a/ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt b/ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt index 09efdd441f9..833115e51ea 100644 --- a/ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt +++ b/ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt @@ -40,11 +40,21 @@ public class TextPart internal constructor( public val text: String, public override val isThought: Boolean, - internal val thoughtSignature: String? + public val thoughtSignature: String? ) : Part { public constructor(text: String) : this(text, false, null) + public companion object { + @JvmStatic + @JvmOverloads + public fun create( + text: String, + isThought: Boolean = false, + thoughtSignature: String? = null + ): TextPart = TextPart(text, isThought, thoughtSignature) + } + @Serializable internal data class Internal( val text: String, @@ -64,7 +74,7 @@ internal constructor( public val outcome: String, public val output: String, public override val isThought: Boolean, - internal val thoughtSignature: String? + public val thoughtSignature: String? ) : Part { @Deprecated("Part of the model response. Do not instantiate directly.") @@ -73,6 +83,19 @@ internal constructor( /** Indicates if the code execution was successful */ public fun executionSucceeded(): Boolean = (outcome.lowercase() == "outcome_ok") + public companion object { + @JvmStatic + @JvmOverloads + @Deprecated("Part of the model response. Do not instantiate directly.") + public fun create( + outcome: String, + output: String, + isThought: Boolean = false, + thoughtSignature: String? = null + ): CodeExecutionResultPart = + CodeExecutionResultPart(outcome, output, isThought, thoughtSignature) + } + @Serializable internal data class Internal( @SerialName("codeExecutionResult") val codeExecutionResult: CodeExecutionResult, @@ -95,12 +118,24 @@ internal constructor( public val language: String, public val code: String, public override val isThought: Boolean, - internal val thoughtSignature: String? + public val thoughtSignature: String? ) : Part { @Deprecated("Part of the model response. Do not instantiate directly.") public constructor(language: String, code: String) : this(language, code, false, null) + public companion object { + @JvmStatic + @JvmOverloads + @Deprecated("Part of the model response. Do not instantiate directly.") + public fun create( + language: String, + code: String, + isThought: Boolean = false, + thoughtSignature: String? = null + ): ExecutableCodePart = ExecutableCodePart(language, code, isThought, thoughtSignature) + } + @Serializable internal data class Internal( @SerialName("executableCode") val executableCode: ExecutableCode, @@ -125,7 +160,7 @@ internal constructor( public val image: Bitmap, public val displayName: String?, public override val isThought: Boolean, - internal val thoughtSignature: String? + public val thoughtSignature: String? ) : Part { /** @param image [Bitmap] to convert into a [Part] */ @@ -134,6 +169,17 @@ internal constructor( /** @param image [Bitmap] to convert into a [Part] */ public constructor(image: Bitmap, displayName: String) : this(image, displayName, false, null) + public companion object { + @JvmStatic + @JvmOverloads + public fun create( + image: Bitmap, + displayName: String? = null, + isThought: Boolean = false, + thoughtSignature: String? = null + ): ImagePart = ImagePart(image, displayName, isThought, thoughtSignature) + } + internal fun toInlineDataPart() = InlineDataPart( android.util.Base64.decode(encodeBitmapToBase64Jpeg(image), BASE_64_FLAGS), @@ -151,7 +197,7 @@ internal constructor( public val mimeType: String, public val displayName: String?, public override val isThought: Boolean, - internal val thoughtSignature: String? + public val thoughtSignature: String? ) : Part { /** @@ -176,6 +222,19 @@ internal constructor( displayName: String ) : this(inlineData, mimeType, displayName, false, null) + public companion object { + @JvmStatic + @JvmOverloads + public fun create( + inlineData: ByteArray, + mimeType: String, + displayName: String? = null, + isThought: Boolean = false, + thoughtSignature: String? = null + ): InlineDataPart = + InlineDataPart(inlineData, mimeType, displayName, isThought, thoughtSignature) + } + @Serializable internal data class Internal( @SerialName("inlineData") val inlineData: InlineData.Internal, @@ -212,7 +271,7 @@ internal constructor( public val args: Map, public val id: String? = null, public override val isThought: Boolean, - internal val thoughtSignature: String? + public val thoughtSignature: String? ) : Part { /** @@ -228,6 +287,18 @@ internal constructor( id: String? = null, ) : this(name, args, id, false, null) + public companion object { + @JvmStatic + @JvmOverloads + public fun create( + name: String, + args: Map, + id: String? = null, + isThought: Boolean = false, + thoughtSignature: String? = null + ): FunctionCallPart = FunctionCallPart(name, args, id, isThought, thoughtSignature) + } + @Serializable internal data class Internal( val functionCall: FunctionCall, @@ -252,7 +323,7 @@ internal constructor( public val id: String? = null, public val parts: List = emptyList(), public override val isThought: Boolean, - internal val thoughtSignature: String? + public val thoughtSignature: String? ) : Part { /** @@ -296,6 +367,18 @@ internal constructor( public fun from(jsonObject: JsonObject, parts: List = emptyList()): FunctionResponsePart { return FunctionResponsePart("", jsonObject, null, parts) } + + @JvmStatic + @JvmOverloads + public fun create( + name: String, + response: JsonObject, + id: String? = null, + parts: List = emptyList(), + isThought: Boolean = false, + thoughtSignature: String? = null + ): FunctionResponsePart = + FunctionResponsePart(name, response, id, parts, isThought, thoughtSignature) } } @@ -305,7 +388,7 @@ internal constructor( public val uri: String, public val mimeType: String, public override val isThought: Boolean, - internal val thoughtSignature: String? + public val thoughtSignature: String? ) : Part { /** @@ -316,6 +399,17 @@ internal constructor( */ public constructor(uri: String, mimeType: String) : this(uri, mimeType, false, null) + public companion object { + @JvmStatic + @JvmOverloads + public fun create( + uri: String, + mimeType: String, + isThought: Boolean = false, + thoughtSignature: String? = null + ): FileDataPart = FileDataPart(uri, mimeType, isThought, thoughtSignature) + } + @Serializable internal data class Internal( @SerialName("file_data") val fileData: FileData, @@ -331,8 +425,13 @@ internal constructor( } } -internal data class UnknownPart(public override val isThought: Boolean = false) : Part { - @Serializable internal data class Internal(val thought: Boolean? = null) : InternalPart +internal data class UnknownPart( + public override val isThought: Boolean = false, + public val thoughtSignature: String? = null +) : Part { + @Serializable + internal data class Internal(val thought: Boolean? = null, val thoughtSignature: String? = null) : + InternalPart } /** Returns the part as a [String] if it represents text, and null otherwise */ @@ -485,7 +584,7 @@ internal fun InternalPart.toPublic(): Part { thoughtSignature ) is FileDataPart.Internal -> - FileDataPart(fileData.mimeType, fileData.fileUri, thought ?: false, thoughtSignature) + FileDataPart(fileData.fileUri, fileData.mimeType, thought ?: false, thoughtSignature) is ExecutableCodePart.Internal -> ExecutableCodePart( executableCode.language, @@ -500,7 +599,7 @@ internal fun InternalPart.toPublic(): Part { thought ?: false, thoughtSignature ) - is UnknownPart.Internal -> UnknownPart() + is UnknownPart.Internal -> UnknownPart(thought ?: false, thoughtSignature) else -> throw com.google.firebase.ai.type.SerializationException( "Unsupported part type \"${javaClass.simpleName}\" provided. This model may not be supported by this SDK." diff --git a/ai-logic/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt b/ai-logic/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt index 4c52056bfdf..ab43dc43a4b 100644 --- a/ai-logic/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt +++ b/ai-logic/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt @@ -665,4 +665,56 @@ internal class SerializationTests { val actualJson = descriptorToJson(LiveServerGoAway.Internal.serializer().descriptor) expectedJsonAsString shouldEqualJson actualJson.toString() } + + @Test + fun `TextPart constructors set isThought and thoughtSignature correctly`() { + val textPartSecondary = com.google.firebase.ai.type.TextPart("hello") + org.junit.Assert.assertEquals(false, textPartSecondary.isThought) + org.junit.Assert.assertEquals(null, textPartSecondary.thoughtSignature) + + val textPartPrimary = + com.google.firebase.ai.type.TextPart.create("hello", true, "signature-xyz") + org.junit.Assert.assertEquals(true, textPartPrimary.isThought) + org.junit.Assert.assertEquals("signature-xyz", textPartPrimary.thoughtSignature) + } + + @Test + fun `FunctionCallPart constructors set isThought and thoughtSignature correctly`() { + val functionCallSecondary = com.google.firebase.ai.type.FunctionCallPart("func", emptyMap()) + org.junit.Assert.assertEquals(false, functionCallSecondary.isThought) + org.junit.Assert.assertEquals(null, functionCallSecondary.thoughtSignature) + + val functionCallPrimary = + com.google.firebase.ai.type.FunctionCallPart.create( + "func", + emptyMap(), + "id-123", + true, + "signature-xyz" + ) + org.junit.Assert.assertEquals(true, functionCallPrimary.isThought) + org.junit.Assert.assertEquals("signature-xyz", functionCallPrimary.thoughtSignature) + } + + @Test + fun `FileDataPart constructors set isThought and thoughtSignature correctly`() { + val fileDataPartSecondary = + com.google.firebase.ai.type.FileDataPart("gs://bucket/file.jpg", "image/jpeg") + org.junit.Assert.assertEquals("gs://bucket/file.jpg", fileDataPartSecondary.uri) + org.junit.Assert.assertEquals("image/jpeg", fileDataPartSecondary.mimeType) + org.junit.Assert.assertEquals(false, fileDataPartSecondary.isThought) + org.junit.Assert.assertEquals(null, fileDataPartSecondary.thoughtSignature) + + val fileDataPartPrimary = + com.google.firebase.ai.type.FileDataPart.create( + "gs://bucket/file.jpg", + "image/jpeg", + true, + "signature-xyz" + ) + org.junit.Assert.assertEquals("gs://bucket/file.jpg", fileDataPartPrimary.uri) + org.junit.Assert.assertEquals("image/jpeg", fileDataPartPrimary.mimeType) + org.junit.Assert.assertEquals(true, fileDataPartPrimary.isThought) + org.junit.Assert.assertEquals("signature-xyz", fileDataPartPrimary.thoughtSignature) + } }