|
| 1 | +/* |
| 2 | + * Copyright 2026 Google LLC |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package com.google.adk.tokt.codecs |
| 18 | + |
| 19 | +import com.google.adk.JsonBaseModel |
| 20 | +import com.google.adk.kt.annotations.FrameworkInternalApi |
| 21 | +import com.google.adk.kt.serialization.anyToJsonElement |
| 22 | +import com.google.adk.kt.serialization.jsonElementToAny |
| 23 | +import com.google.adk.kt.types.Content as KtContent |
| 24 | +import com.google.adk.kt.workflow.NodeInfo as KtNodeInfo |
| 25 | +import com.google.adk.kt.workflow.Route as KtRoute |
| 26 | +import com.google.adk.workflow.NodeInfo as JavaNodeInfo |
| 27 | +import com.google.adk.workflow.Route as JavaRoute |
| 28 | +import com.google.genai.types.Content as GenaiContent |
| 29 | + |
| 30 | +/** |
| 31 | + * Converts a workflow node's output, its [NodeInfo][KtNodeInfo], and the routes on `actions.route` |
| 32 | + * between ADK Kotlin and the ADK Java facade. |
| 33 | + */ |
| 34 | +internal object WorkflowCodec { |
| 35 | + |
| 36 | + /** |
| 37 | + * Returns the Java form of a node's [output]: a Kotlin `Content` becomes a genai `Content`, and a |
| 38 | + * kotlinx `JsonElement` becomes the plain maps, lists, and values of an event read from JSON. |
| 39 | + * Maps and lists are copied with each entry converted; a value with no JSON form throws. |
| 40 | + */ |
| 41 | + @OptIn(FrameworkInternalApi::class) |
| 42 | + fun outputToJava(output: Any?): Any? = |
| 43 | + when (output) { |
| 44 | + null, |
| 45 | + is String, |
| 46 | + is Boolean, |
| 47 | + is Number -> output |
| 48 | + is KtContent -> ContentCodec.toJava(output) |
| 49 | + // A JsonObject or JsonArray is also a Map or List, so it converts entry by entry here. |
| 50 | + is Map<*, *> -> |
| 51 | + output.entries.associate { (key, value) -> key.toString() to outputToJava(value) } |
| 52 | + is List<*> -> output.map { outputToJava(it) } |
| 53 | + // Unwraps a JSON primitive or JSON null; throws if a value has no JSON form. |
| 54 | + else -> jsonElementToAny(anyToJsonElement(output)) |
| 55 | + } |
| 56 | + |
| 57 | + /** |
| 58 | + * Returns the Kotlin form of a node's [output]: a genai `Content` becomes a Kotlin `Content`, and |
| 59 | + * any other non-JSON-native object becomes its JSON form, as in an event read from JSON. Maps and |
| 60 | + * lists are copied with each entry converted. |
| 61 | + */ |
| 62 | + fun outputFromJava(output: Any?): Any? = |
| 63 | + when (output) { |
| 64 | + null, |
| 65 | + is String, |
| 66 | + is Boolean, |
| 67 | + is Int, |
| 68 | + is Long, |
| 69 | + is Float, |
| 70 | + is Double, |
| 71 | + is Short, |
| 72 | + is Byte -> output |
| 73 | + // ADK Kotlin has no JSON form for BigDecimal or BigInteger, so convert to Long or Double. |
| 74 | + is Number -> output.toString().toLongOrNull() ?: output.toDouble() |
| 75 | + is GenaiContent -> ContentCodec.fromJava(output) |
| 76 | + is Map<*, *> -> |
| 77 | + output.entries.associate { (key, value) -> key.toString() to outputFromJava(value) } |
| 78 | + is List<*> -> output.map { outputFromJava(it) } |
| 79 | + is Array<*> -> output.map { outputFromJava(it) } |
| 80 | + // Write, then read: convertValue can return an embedded value, such as a byte[], unchanged. |
| 81 | + else -> |
| 82 | + outputFromJava( |
| 83 | + JsonBaseModel.getMapper().let { |
| 84 | + it.readValue(it.writeValueAsString(output), Any::class.java) |
| 85 | + } |
| 86 | + ) |
| 87 | + } |
| 88 | + |
| 89 | + /** Returns the Java [JavaNodeInfo] copy of the Kotlin [info]. */ |
| 90 | + fun nodeInfoToJava(info: KtNodeInfo): JavaNodeInfo = |
| 91 | + JavaNodeInfo.builder() |
| 92 | + .path(info.path) |
| 93 | + .outputFor(info.outputFor.orEmpty()) |
| 94 | + .messageAsOutput(info.messageAsOutput) |
| 95 | + .build() |
| 96 | + |
| 97 | + /** |
| 98 | + * Returns the Kotlin [KtNodeInfo] copy of the Java [info]. An empty `outputFor` reads as null, |
| 99 | + * the Kotlin default, since the Java list is never null. |
| 100 | + */ |
| 101 | + fun nodeInfoFromJava(info: JavaNodeInfo): KtNodeInfo = |
| 102 | + KtNodeInfo( |
| 103 | + path = info.path(), |
| 104 | + outputFor = info.outputFor().ifEmpty { null }, |
| 105 | + messageAsOutput = info.messageAsOutput(), |
| 106 | + ) |
| 107 | + |
| 108 | + /** Returns the Java [JavaRoute] copy of the Kotlin [route]. */ |
| 109 | + fun routeToJava(route: KtRoute): JavaRoute = |
| 110 | + when (route) { |
| 111 | + is KtRoute.Tag -> JavaRoute.Tag(route.value) |
| 112 | + is KtRoute.Num -> JavaRoute.Num(route.value) |
| 113 | + is KtRoute.Flag -> JavaRoute.Flag(route.value) |
| 114 | + KtRoute.Default -> JavaRoute.Default.INSTANCE |
| 115 | + } |
| 116 | + |
| 117 | + /** Returns the Kotlin [KtRoute] copy of the Java [route]. */ |
| 118 | + fun routeFromJava(route: JavaRoute): KtRoute = |
| 119 | + when (route) { |
| 120 | + is JavaRoute.Tag -> KtRoute.Tag(route.value()) |
| 121 | + is JavaRoute.Num -> KtRoute.Num(route.value()) |
| 122 | + is JavaRoute.Flag -> KtRoute.Flag(route.value()) |
| 123 | + is JavaRoute.Default -> KtRoute.Default |
| 124 | + } |
| 125 | +} |
0 commit comments