Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 83 additions & 11 deletions tokt/src/main/kotlin/com/google/adk/tokt/codecs/RunConfigCodec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@
package com.google.adk.tokt.codecs

import com.google.adk.agents.RunConfig as JavaRunConfig
import com.google.adk.kt.agents.LiveRunConfig as KtLiveRunConfig
import com.google.adk.kt.agents.RunConfig as KtRunConfig
import com.google.adk.kt.agents.StreamingMode as KtStreamingMode
import com.google.adk.kt.annotations.ExperimentalLiveApi
import com.google.adk.kt.types.AudioTranscriptionConfig as KtAudioTranscriptionConfig
import com.google.adk.kt.types.Modality as KtModality
import com.google.genai.types.Modality as GenaiModality

/**
* Converts between the Kotlin `kt.agents.RunConfig` and the ADK Java [JavaRunConfig].
*
* Streaming mode, the LLM call budget and custom metadata cross both ways; [toJava] also shows a
* BIDI run's live settings (modalities, speech, transcriptions). [fromJava] rejects a Java setting
* the engine cannot honor rather than dropping it silently.
* BIDI run's live settings (modalities, speech, transcriptions), and [fromJavaLive] reads them for
* a live run. [fromJava] and [fromJavaLive] reject a Java setting the engine cannot honor rather
* than dropping it silently.
*/
internal object RunConfigCodec {

Expand Down Expand Up @@ -63,23 +67,15 @@ internal object RunConfigCodec {
}

/** Returns the Kotlin [KtRunConfig] view of the Java [config]. */
@Suppress(
"deprecation"
) // Reads the deprecated groupFunctionResponsesInHistoryOverride to reject it.
fun fromJava(config: JavaRunConfig): KtRunConfig {
val unsupported = buildList {
if (config.streamingMode() == JavaRunConfig.StreamingMode.BIDI) add("streamingMode=BIDI")
if (config.saveInputBlobsAsArtifacts()) add("saveInputBlobsAsArtifacts")
if (config.toolExecutionMode() != JavaRunConfig.ToolExecutionMode.NONE)
add("toolExecutionMode")
addUnsupportedByEveryRun(config)
if (config.responseModalities().isNotEmpty()) add("responseModalities")
if (config.speechConfig() != null) add("speechConfig")
if (config.avatarConfig() != null) add("avatarConfig")
if (config.outputAudioTranscription() != null) add("outputAudioTranscription")
if (config.inputAudioTranscription() != null) add("inputAudioTranscription")
if (config.groupFunctionResponsesInHistoryOverride().isPresent)
add("groupFunctionResponsesInHistoryOverride")
if (config.dollarEscapedPlaceholders()) add("dollarEscapedPlaceholders")
}
require(unsupported.isEmpty()) {
"RunConfig settings not supported by the ADK Kotlin engine: $unsupported"
Expand All @@ -92,4 +88,80 @@ internal object RunConfigCodec {
customMetadata = config.customMetadata().takeIf { it.isNotEmpty() },
)
}

/**
* Returns the Kotlin [KtRunConfig] for a live run, applying the ADK Java runner's live defaults:
* no modality means AUDIO, and a missing transcription setting turns transcription on, except
* output transcription when the modalities include text. Rejects a Java setting the engine cannot
* honor, as [fromJava] does, but ignores `saveInputBlobsAsArtifacts`, which ADK Java's own live
* run never reads.
*/
@OptIn(ExperimentalLiveApi::class)
fun fromJavaLive(config: JavaRunConfig): KtRunConfig {
val output = config.outputAudioTranscription()
val input = config.inputAudioTranscription()
val unsupported = buildList {
addUnsupportedByEveryRun(config)
if (output != null) {
addAll(output.unsupportedFields.map { "outputAudioTranscription.$it" })
}
if (input != null) {
addAll(input.unsupportedFields.map { "inputAudioTranscription.$it" })
}
}
require(unsupported.isEmpty()) {
"RunConfig settings not supported by the ADK Kotlin engine: $unsupported"
}
val modalities =
config.responseModalities().map { modality ->
// genai reads any name it does not know as MODALITY_UNSPECIFIED.
requireNotNull(
enumByNameOrNull<KtModality>(modality.knownEnum().name)?.takeIf {
it != KtModality.MODALITY_UNSPECIFIED
}
) {
"Response modality not supported by the ADK Kotlin engine"
}
}
// The ADK Java runner's live defaults (Runner.newInvocationContextForLive).
val responseModalities = modalities.ifEmpty { listOf(KtModality.AUDIO) }
val textRequested = KtModality.TEXT in responseModalities
return KtRunConfig(
// ADK Java's runLive keeps the given mode; BIDI is what shows Java plugins the live settings.
streamingMode = KtStreamingMode.BIDI,
maxLlmCalls = config.maxLlmCalls(),
customMetadata = config.customMetadata().ifEmpty { null },
live =
KtLiveRunConfig(
responseModalities = responseModalities,
speechConfig = config.speechConfig()?.toKt(),
outputAudioTranscription =
when {
output != null -> output.toKt()
textRequested -> null
else -> KtAudioTranscriptionConfig()
},
inputAudioTranscription =
if (input != null) {
input.toKt()
} else {
KtAudioTranscriptionConfig()
},
),
)
}

/** Adds the Java settings that neither a turn-based nor a live run can honor. */
// Reads the deprecated groupFunctionResponsesInHistoryOverride to reject it.
@Suppress("deprecation")
private fun MutableList<String>.addUnsupportedByEveryRun(config: JavaRunConfig) {
if (config.toolExecutionMode() != JavaRunConfig.ToolExecutionMode.NONE) {
add("toolExecutionMode")
}
if (config.avatarConfig() != null) add("avatarConfig")
if (config.groupFunctionResponsesInHistoryOverride().isPresent) {
add("groupFunctionResponsesInHistoryOverride")
}
if (config.dollarEscapedPlaceholders()) add("dollarEscapedPlaceholders")
}
}
160 changes: 160 additions & 0 deletions tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ import com.google.common.truth.Truth.assertThat
import com.google.errorprone.annotations.CanIgnoreReturnValue
import com.google.genai.types.AudioTranscriptionConfig as GenaiAudioTranscriptionConfig
import com.google.genai.types.AudioTranscriptionConfigMode as GenaiAudioTranscriptionConfigMode
import com.google.genai.types.AvatarConfig as GenaiAvatarConfig
import com.google.genai.types.CodeExecutionResult as GenaiCodeExecutionResult
import com.google.genai.types.Content as GenaiContent
import com.google.genai.types.CustomMetadata as GenaiCustomMetadata
Expand Down Expand Up @@ -2062,6 +2063,145 @@ class KtRunnerInteropTest {
}
}

@Test
fun runConfigFromJavaLive_appliesTheJavaRunnersLiveDefaults() {
val audio = RunConfigCodec.fromJavaLive(JavaRunConfig.builder().build())
val text =
RunConfigCodec.fromJavaLive(
JavaRunConfig.builder()
.responseModalities(listOf(GenaiModality(GenaiModality.Known.TEXT)))
.build()
)

assertEquals(KtStreamingMode.BIDI, audio.streamingMode)
assertEquals(listOf(KtModality.AUDIO), audio.live.responseModalities, "no modality means AUDIO")
assertEquals(KtAudioTranscriptionConfig(), audio.live.inputAudioTranscription)
assertEquals(KtAudioTranscriptionConfig(), audio.live.outputAudioTranscription)
assertNull(audio.live.speechConfig)
assertNull(audio.customMetadata)
assertEquals(listOf(KtModality.TEXT), text.live.responseModalities)
assertEquals(KtAudioTranscriptionConfig(), text.live.inputAudioTranscription)
assertNull(text.live.outputAudioTranscription, "a text reply turns output transcription off")
}

@Test
fun runConfigFromJavaLive_speechAndTranscriptionSettings_convert() {
val java =
JavaRunConfig.builder()
.speechConfig(genaiSpeechConfig())
.outputAudioTranscription(genaiTranscriptionConfig())
.inputAudioTranscription(
GenaiAudioTranscriptionConfig.builder().languageCodes(listOf("de-DE")).build()
)
.build()

val kt = RunConfigCodec.fromJavaLive(java)

assertEquals(ktSpeechConfig(), kt.live.speechConfig)
assertEquals(ktTranscriptionConfig(), kt.live.outputAudioTranscription)
assertEquals(
KtAudioTranscriptionConfig(languageCodes = listOf("de-DE")),
kt.live.inputAudioTranscription,
)
}

@Test
@Suppress("DEPRECATION") // Sets the deprecated grouping override to check that it is rejected.
fun runConfigFromJavaLive_settingsTheEngineCannotHonor_failNamingThem() {
val java =
JavaRunConfig.builder()
.toolExecutionMode(JavaRunConfig.ToolExecutionMode.SEQUENTIAL)
.avatarConfig(GenaiAvatarConfig.builder().avatarName("a").build())
.groupFunctionResponsesInHistoryOverride(Optional.of(true))
.dollarEscapedPlaceholders(true)
.outputAudioTranscription(
GenaiAudioTranscriptionConfig.builder()
.languageAuto(GenaiLanguageAuto.builder().build())
.languageHints(GenaiLanguageHints.builder().languageCodes(listOf("pl-PL")).build())
.adaptationPhrases(listOf("ADK"))
.mode(GenaiAudioTranscriptionConfigMode("m"))
.build()
)
.inputAudioTranscription(
GenaiAudioTranscriptionConfig.builder().adaptationPhrases(listOf("ADK")).build()
)
.build()

val failure = assertFailsWith<IllegalArgumentException> { RunConfigCodec.fromJavaLive(java) }

assertEquals(
"RunConfig settings not supported by the ADK Kotlin engine: [toolExecutionMode, " +
"avatarConfig, groupFunctionResponsesInHistoryOverride, dollarEscapedPlaceholders, " +
"outputAudioTranscription.languageAuto, outputAudioTranscription.languageHints, " +
"outputAudioTranscription.adaptationPhrases, outputAudioTranscription.mode, " +
"inputAudioTranscription.adaptationPhrases]",
failure.message,
)
}

@Test
fun runConfigFromJavaLive_ignoresSaveInputBlobsAsArtifacts() {
// ADK Java's own live run never reads it, so a live run it would start must start here too.
val kt =
RunConfigCodec.fromJavaLive(JavaRunConfig.builder().saveInputBlobsAsArtifacts(true).build())

assertEquals(KtStreamingMode.BIDI, kt.streamingMode)
}

@Test
fun runConfigFromJavaLive_unknownModality_fails() {
// genai reads a name it does not know as MODALITY_UNSPECIFIED, which must not reach the model.
val java = JavaRunConfig.builder().responseModalities(listOf(GenaiModality("DOCUMENT"))).build()

val failure = assertFailsWith<IllegalArgumentException> { RunConfigCodec.fromJavaLive(java) }

assertEquals("Response modality not supported by the ADK Kotlin engine", failure.message)
}

@Test
fun runConfigFromJavaLive_carriesTheCallBudgetAndMetadata() {
val kt =
RunConfigCodec.fromJavaLive(
JavaRunConfig.builder().maxLlmCalls(7).customMetadata(mapOf("k" to "v")).build()
)

assertEquals(7, kt.maxLlmCalls)
assertEquals<Map<String, Any?>?>(mapOf("k" to "v"), kt.customMetadata)
}

@Test
fun runConfigFromJavaLive_outputTranscriptionDefault_followsTheJavaRunnersTextRule() {
// Keep an explicit setting with TEXT, and apply a default only when no modality is TEXT.
val textWithOutput =
RunConfigCodec.fromJavaLive(
JavaRunConfig.builder()
.responseModalities(listOf(GenaiModality(GenaiModality.Known.TEXT)))
.outputAudioTranscription(genaiTranscriptionConfig())
.build()
)
val audioAndText =
RunConfigCodec.fromJavaLive(
JavaRunConfig.builder()
.responseModalities(
listOf(
GenaiModality(GenaiModality.Known.AUDIO),
GenaiModality(GenaiModality.Known.TEXT),
)
)
.build()
)
val audioOnly =
RunConfigCodec.fromJavaLive(
JavaRunConfig.builder()
.responseModalities(listOf(GenaiModality(GenaiModality.Known.AUDIO)))
.build()
)

assertEquals(ktTranscriptionConfig(), textWithOutput.live.outputAudioTranscription)
assertNull(audioAndText.live.outputAudioTranscription)
assertEquals(KtAudioTranscriptionConfig(), audioOnly.live.outputAudioTranscription)
}

@Test
fun runConfigToJava_bidiWithLiveSettings_showsLiveSettings() {
val kt =
Expand Down Expand Up @@ -6069,6 +6209,26 @@ class KtRunnerInteropTest {
)
}

@Test
@Suppress("DEPRECATION") // Sets the deprecated grouping override to check that it is rejected.
fun runConfigFromJava_settingsNoRunCanHonor_failNamingThem() {
val java =
JavaRunConfig.builder()
.toolExecutionMode(JavaRunConfig.ToolExecutionMode.SEQUENTIAL)
.avatarConfig(GenaiAvatarConfig.builder().avatarName("a").build())
.groupFunctionResponsesInHistoryOverride(Optional.of(true))
.dollarEscapedPlaceholders(true)
.build()

val failure = assertFailsWith<IllegalArgumentException> { RunConfigCodec.fromJava(java) }

assertEquals(
"RunConfig settings not supported by the ADK Kotlin engine: [toolExecutionMode, " +
"avatarConfig, groupFunctionResponsesInHistoryOverride, dollarEscapedPlaceholders]",
failure.message,
)
}

@Test
fun runConfigFromJava_unsupportedField_failsRatherThanDroppingIt() {
val java = JavaRunConfig.builder().maxLlmCalls(5).saveInputBlobsAsArtifacts(true).build()
Expand Down
Loading