diff --git a/core/src/commonJvmAndroidMain/kotlin/com/google/adk/kt/types/GenaiConverters.kt b/core/src/commonJvmAndroidMain/kotlin/com/google/adk/kt/types/GenaiConverters.kt index 2e50af23..61932bd4 100644 --- a/core/src/commonJvmAndroidMain/kotlin/com/google/adk/kt/types/GenaiConverters.kt +++ b/core/src/commonJvmAndroidMain/kotlin/com/google/adk/kt/types/GenaiConverters.kt @@ -21,6 +21,8 @@ package com.google.adk.kt.types import kotlin.jvm.optionals.getOrNull +import kotlin.time.toJavaDuration +import kotlin.time.toKotlinDuration // --- Blob --- /** Converts a [com.google.genai.types.Blob] from the GenAI SDK to an ADK [Blob]. */ @@ -702,6 +704,8 @@ internal fun com.google.genai.types.Part.fromGenaiSdk(): Part = functionResponse = functionResponse().getOrNull()?.fromGenaiSdk(), thought = thought().getOrNull(), thoughtSignature = thoughtSignature().getOrNull(), + videoMetadata = videoMetadata().getOrNull()?.fromGenaiSdk(), + partMetadata = partMetadata().getOrNull(), ) /** Converts an ADK [Part] to a [com.google.genai.types.Part] for the GenAI SDK. */ @@ -715,6 +719,31 @@ internal fun Part.toGenaiSdk(): com.google.genai.types.Part = this@toGenaiSdk.functionResponse?.let { functionResponse(it.toGenaiSdk()) } this@toGenaiSdk.thought?.let { thought(it) } this@toGenaiSdk.thoughtSignature?.let { thoughtSignature(it) } + this@toGenaiSdk.videoMetadata?.let { videoMetadata(it.toGenaiSdk()) } + this@toGenaiSdk.partMetadata?.let { partMetadata(it) } + } + .build() + +// --- VideoMetadata --- +/** + * Converts a [com.google.genai.types.VideoMetadata] from the GenAI SDK to an ADK [VideoMetadata]. + */ +internal fun com.google.genai.types.VideoMetadata.fromGenaiSdk(): VideoMetadata = + VideoMetadata( + startOffset = startOffset().getOrNull()?.toKotlinDuration(), + endOffset = endOffset().getOrNull()?.toKotlinDuration(), + fps = fps().getOrNull(), + ) + +/** + * Converts an ADK [VideoMetadata] to a [com.google.genai.types.VideoMetadata] for the GenAI SDK. + */ +internal fun VideoMetadata.toGenaiSdk(): com.google.genai.types.VideoMetadata = + com.google.genai.types.VideoMetadata.builder() + .apply { + this@toGenaiSdk.startOffset?.let { startOffset(it.toJavaDuration()) } + this@toGenaiSdk.endOffset?.let { endOffset(it.toJavaDuration()) } + this@toGenaiSdk.fps?.let { fps(it) } } .build() diff --git a/core/src/commonJvmAndroidTest/kotlin/com/google/adk/kt/types/GenaiConvertersTest.kt b/core/src/commonJvmAndroidTest/kotlin/com/google/adk/kt/types/GenaiConvertersTest.kt index 3b39c9c4..b267226b 100644 --- a/core/src/commonJvmAndroidTest/kotlin/com/google/adk/kt/types/GenaiConvertersTest.kt +++ b/core/src/commonJvmAndroidTest/kotlin/com/google/adk/kt/types/GenaiConvertersTest.kt @@ -20,6 +20,7 @@ import com.google.adk.kt.testing.userMessage import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.seconds class GenaiConvertersTest { @Test @@ -540,6 +541,22 @@ class GenaiConvertersTest { assertEquals(adkPart, convertedBack) } + @Test + fun part_videoMetadataAndPartMetadata_convertsCorrectly() { + val adkPart = + Part( + text = "clip", + videoMetadata = VideoMetadata(startOffset = 1.seconds, endOffset = 5.seconds, fps = 24.0), + partMetadata = mapOf("source" to "camera-1"), + ) + + val genaiPart = adkPart.toGenaiSdk() + assertEquals(24.0, genaiPart.videoMetadata().get().fps().get()) + + val convertedBack = genaiPart.fromGenaiSdk() + assertEquals(adkPart, convertedBack) + } + @Test fun partialArg_null_convertsCorrectly() { val adkPartialArgNull = diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/types/Part.kt b/core/src/commonMain/kotlin/com/google/adk/kt/types/Part.kt index 7012d382..dac6442a 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/types/Part.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/types/Part.kt @@ -16,6 +16,7 @@ package com.google.adk.kt.types import com.google.adk.kt.annotations.FrameworkInternalApi +import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -49,6 +50,10 @@ constructor( val thought: Boolean? = null, /** An opaque signature for the thought. */ val thoughtSignature: ByteArray? = null, + /** Metadata for a video part (segment offsets and frame rate). */ + val videoMetadata: VideoMetadata? = null, + /** Arbitrary key-value metadata associated with this part. The map must be JSON serializable. */ + val partMetadata: Map? = null, /** * Other opaque data associated with the part to be interpreted by the agent. Reserved for ADK * internal use. Users should not set this field. @@ -68,6 +73,8 @@ constructor( functionResponse: FunctionResponse? = null, thought: Boolean? = null, thoughtSignature: ByteArray? = null, + videoMetadata: VideoMetadata? = null, + partMetadata: Map? = null, ) : this( text, inlineData, @@ -76,6 +83,8 @@ constructor( functionResponse, thought, thoughtSignature, + videoMetadata, + partMetadata, opaqueData = null, ) @@ -91,6 +100,8 @@ constructor( functionResponse == other.functionResponse && thought == other.thought && thoughtSignature.contentEquals(other.thoughtSignature) && + videoMetadata == other.videoMetadata && + partMetadata == other.partMetadata && opaqueData == other.opaqueData } @@ -104,6 +115,8 @@ constructor( result = 31 * result + (thought?.hashCode() ?: 0) result = 31 * result + (thoughtSignature?.contentHashCode() ?: 0) + result = 31 * result + (videoMetadata?.hashCode() ?: 0) + result = 31 * result + (partMetadata?.hashCode() ?: 0) result = 31 * result + (opaqueData?.hashCode() ?: 0) return result } @@ -117,6 +130,8 @@ constructor( functionResponse: FunctionResponse? = this.functionResponse, thought: Boolean? = this.thought, thoughtSignature: ByteArray? = this.thoughtSignature, + videoMetadata: VideoMetadata? = this.videoMetadata, + partMetadata: Map? = this.partMetadata, opaqueData: Any?, ): Part = Part( @@ -127,6 +142,8 @@ constructor( functionResponse, thought, thoughtSignature, + videoMetadata, + partMetadata, opaqueData, ) @@ -139,6 +156,8 @@ constructor( functionResponse: FunctionResponse? = this.functionResponse, thought: Boolean? = this.thought, thoughtSignature: ByteArray? = this.thoughtSignature, + videoMetadata: VideoMetadata? = this.videoMetadata, + partMetadata: Map? = this.partMetadata, ): Part = Part( text, @@ -148,6 +167,8 @@ constructor( functionResponse, thought, thoughtSignature, + videoMetadata, + partMetadata, opaqueData, ) diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/types/VideoMetadata.kt b/core/src/commonMain/kotlin/com/google/adk/kt/types/VideoMetadata.kt new file mode 100644 index 00000000..ee32c5a4 --- /dev/null +++ b/core/src/commonMain/kotlin/com/google/adk/kt/types/VideoMetadata.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2026 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.adk.kt.types + +import kotlin.time.Duration +import kotlinx.serialization.Serializable + +/** Metadata describing how to interpret a video [Part]. */ +@Serializable +data class VideoMetadata( + /** The start offset of the video segment to use. */ + val startOffset: Duration? = null, + /** The end offset of the video segment to use. */ + val endOffset: Duration? = null, + /** The frame rate (frames per second) to sample the video at. */ + val fps: Double? = null, +)